Kotlin-安卓开发-全-

Kotlin 安卓开发(全)

原文:zh.annas-archive.org/md5/5516731C6537B7140E922B2C519B8673

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

如今,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 codeeffective Java)以及有关该语言使用情况的数据的分析。对其他编程语言进行了深入分析。Kotlin 努力避免重复其他语言的错误,并利用它们最有用的特性。在使用 Kotlin 时,我们真的可以感觉到这是一种成熟且设计良好的语言。

Kotlin 通过提高代码质量和安全性以及提高开发人员的性能,将应用程序开发提升到一个全新的水平。Google 在 2017 年宣布正式支持 Android 平台的 Kotlin,但 Kotlin 语言已经存在一段时间了。它拥有一个非常活跃的社区,而且在 Android 平台上的 Kotlin 采用已经迅速增长。我们可以将 Kotlin 描述为一种安全、表达力强、简洁、多功能且友好的语言,它与 Java 和 JavaScript 具有很好的互操作性。让我们讨论一下这些特点:

  • 安全性:Kotlin 在空指针和不可变性方面提供了安全功能。Kotlin 是静态类型的,因此在编译时就知道每个表达式的类型。编译器可以验证我们尝试访问的任何属性或方法或特定类实例是否真的存在。这应该是熟悉的 Java,它也是静态类型的,但与 Java 不同,Kotlin 的类型系统更加严格(安全)。我们必须明确告诉编译器给定的变量是否可以存储空值。这允许在编译时使程序失败,而不是在运行时抛出NullPointerException

  • 易于调试:在开发阶段可以更快地检测到错误,而不是在发布后导致应用程序崩溃,从而损害用户体验。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 通过提供一组接口和辅助方法(ListMutableListSetSetMutableSetMapMutableMap等)区分可变和不可变集合:

    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中执行的书中代码示例。

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 并按下安装按钮:

要使用 Kotlin,我们需要在项目中配置 Kotlin。对于现有的 Java 项目,我们需要运行在项目中配置 Kotlin操作(在 Windows 中的快捷键是Ctrl +Shift +A,在 macOS 中是command + shift + A)或者使用相应的工具 |Kotlin | 在项目中配置 Kotlin 菜单项:

然后,选择 Android with Gradle:

最后,我们需要选择所需的模块和适当的 Kotlin 版本:

前面的配置场景也适用于最初是用 Java 创建的所有现有 Android 项目。从 Android Studio 3.0 开始,我们还可以在创建新项目时勾选包括 Kotlin 支持的复选框:

在这两种情况下,在项目中配置 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 才能更新其配置并下载所有所需的依赖项:

在新的 Android 项目中使用 Kotlin

在 Android Studio 3.x 中创建的新 Kotlin 项目中,主要活动将已经在 Kotlin 中定义,因此我们可以立即开始编写 Kotlin 代码:

添加新的 Kotlin 文件类似于添加 Java 文件。只需右键单击包,然后选择新建|Kotlin 文件/类:

IDE 之所以说 Kotlin 文件/类而不是简单的Kotlin 类,类似于Java 类,是因为我们可以在单个文件中定义更多成员。我们将在第二章中更详细地讨论这个问题,奠定基础

请注意,Kotlin 源文件可以位于java源文件夹内。我们可以为 Kotlin 创建一个新的源文件夹,但这并不是必需的:

运行和调试项目与 Java 完全相同,除了在项目中配置 Kotlin 之外,不需要任何额外的步骤:

从 Android Studio 3.0 开始,各种 Android 模板也将允许我们选择一种语言。这是新的配置活动向导:

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,它读取单个用户输入,评估它,并打印结果:

REPL 看起来像命令行,但它将为我们提供所有所需的代码提示,并且让我们访问项目内定义的各种结构(类、接口、顶级函数等):

REPL 的最大优势是速度。我们可以非常快速地测试 Kotlin 代码。

Kotlin 的内部工作原理

我们主要将重点放在 Android 上,但请记住,Kotlin 可以编译到多个平台。Kotlin 代码可以编译为Java 字节码,然后再编译为Dalvik 字节码。以下是 Android 平台的 Kotlin 构建过程的简化版本:

  • 扩展名为.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-runtimekotlin-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):

语言的初始名称是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 中,我们有两种类型的变量:varval。第一种var是可变引用(读写),可以在初始化后更新。var关键字用于定义 Kotlin 中的变量。它相当于普通(非 final)的 Java 变量。如果我们的变量需要在某个时候更改,我们应该使用var关键字进行声明。让我们看一个变量声明的例子:

    fun main(args: Array<String>) { 
        var fruit:String =  "orange" //1 
        fruit  = "banana" //2 
    } 
  1. 创建水果变量,并用变量orange的值进行初始化

  2. 重新初始化水果变量为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 
    } 
  1. 创建水果变量,并用字符串orange的值进行初始化

  2. 编译器会抛出错误,因为水果变量已经被初始化

Kotlin 还允许我们在文件级别定义变量和函数。我们将在第三章“玩转函数”中进一步讨论。

请注意,变量引用的类型(varval)与引用本身有关,而不是所引用对象的属性。这意味着当使用只读引用(val)时,我们将无法更改指向特定对象实例的引用(我们将无法重新分配变量值),但我们仍然可以修改所引用对象的属性。让我们使用数组来看看它的运行情况:

    val list = mutableListOf("a","b","c") //1 
    list = mutableListOf("d", "e") //2 Error 
    list.remove("a") //3 
  1. 初始化可变列表

  2. 编译器会抛出错误,因为值引用不能被改变(重新分配)

  3. 编译器将允许修改列表的内容

关键字val不能保证底层对象是不可变的。

如果我们真的想确保对象不会被修改,我们必须使用不可变引用和不可变对象。幸运的是,Kotlin 的标准库包含了任何集合接口(ListMutableListMapMutableMap等)的不可变等价物,对于用于创建特定集合实例的辅助函数也是如此:

变量/值定义 引用可以改变 对象状态可以改变
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 
  1. 推断类型为String,我们正在尝试分配Int

如果我们想要将Int(值12)赋给标题变量,那么我们需要指定标题类型为StringInt的公共类型。在类型层次结构中最接近的一个是Any

    var title: Any = "Kotlin" 
    title = 12

Any 是 Java 对象类型的等价物。它是 Kotlin 类型层次结构的根。Kotlin 中的所有类都明确继承自类型Any,甚至是StringInt等原始类型

Any 定义了三个方法:equalstoStringhashCode。Kotlin 标准库包含了这种类型的一些扩展。我们将在第七章 扩展函数和属性中讨论扩展。

正如我们所看到的,类型推断 不仅限于原始值。让我们直接从函数中推断类型:

    var total = sum(10, 20) 

在前面的例子中,推断的类型将与函数返回的类型相同。我们可能猜测它将是Int,但它也可能是DoubleFloat或其他类型。如果从上下文中不明显推断出类型,我们可以在变量名上放置插入符号,并运行 Android Studio 的表达式类型命令(对于 Windows,是Shift + Ctrl + P,对于 macOS,是箭头键+ control + P)。这将在工具提示中显示变量类型,如下所示:

类型推断也适用于泛型类型:

    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 编译器将尝试为所有对推断出一个公共类型。两对中的第一个参数都是StringMount EverestK2),因此在这里自然会推断出String。每对的第二个参数不同(第一对为Int,第二对为String),因此 Kotlin 需要找到最接近的公共类型。选择Any类型,因为这是上游类型层次结构中最接近的公共类型:

正如我们所看到的,类型推断在大多数情况下都做得很好,但是我们仍然可以选择显式定义数据类型,例如,我们想要不同的变量类型:

    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 
  1. 编译器会抛出错误,因为这种类型不允许为 null。

  2. 编译器将允许空赋值,因为类型使用问号后缀标记为可空。

我们不允许在可能为 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 的子类型:

Nothing 类型是一个空类型(无法实例化的类型),它不能有实例。我们将在 第三章 Playing with Functions 中详细讨论它。这种类型层次结构是为什么我们可以将非空对象(Vehicle)赋值给可空类型的变量(Vehicle?),但不能将可空对象(Vehicle?)赋值给非空变量(Vehicle)的原因:

    var nullableVehicle: Vehicle?  
    var vehicle: Vehicle 

    nullableVehicle = vehicle // 1 
    vehicle = nullableVehicle // 2, Error 

  1. 可以进行赋值

  2. 错误,因为 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 
    } 
  1. savedInstanceState 定义为可空的 Bundle?

  2. 编译器会抛出错误

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") 
    } 

如果savedInstanceStatenull,那么将返回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。这意味着该值将始终被返回,因此推断出非空布尔类型。

该运算符的名称来自著名的美国歌手兼词曲作家埃尔维斯·普雷斯利,因为他的发型类似于问号。

非空断言

处理空值的另一个工具是非空断言运算符。它由双感叹号(!!)表示。该运算符将可空变量显式转换为非空变量。以下是一个使用示例:

    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 
        } 
    } 
  1. let中的savedInstanceState可以使用变量访问

命名它。

如前所述,安全调用运算符的右侧表达式只有在左侧不为空时才会被评估。在这种情况下,右侧是一个接受另一个函数(lambda)作为参数的let函数。如果savedInstanceState不为空,则将执行let后面的代码块。我们将在第七章中学习更多关于它以及如何定义这样的函数,扩展函数和属性

空值和 Java

我们知道 Kotlin 要求明确定义可以保存空值的引用。另一方面,Java 对空值的处理更为宽松,因此我们可能会想知道 Kotlin 如何处理来自 Java 的类型(基本上是整个 Android SDK 和用 Java 编写的库)。在可能的情况下,Kotlin 编译器将从代码中确定类型的空值,并使用空值注解将类型表示为实际的可空或非可空类型。

Kotlin 编译器支持多种空值注解,包括:

  • Android (com.android.annotationsandroid.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 
  1. 假设textView在每个配置的每个布局中都存在,因此textView可以定义为非可空的。

  2. 假设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

我们将在原始数据类型部分讨论原始类型及其转换。

智能转换

智能转换将一个类型的变量转换为另一个类型,但与安全转换相反,它是隐式完成的(我们不需要使用asas?转换操作符)。智能转换仅在 Kotlin 编译器绝对确定变量在检查后不会被更改时才有效。这使它们非常适用于多线程应用程序。通常,智能转换适用于所有不可变引用(val)和本地可变引用(var)。我们有两种智能转换:

  • 类型智能转换将一个类型的对象转换为另一个类型的对象

  • 将可空引用转换为非空引用的空值智能转换

类型智能转换

让我们表示上一节中的AnimalFish类:

假设我们想调用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 会用绿色背景标记变量。

在 Kotlin 中,我们不必显式将animal实例转换为Fish,因为在类型检查后,Kotlin 编译器将能够隐式处理转换。现在,在if块内,变量animal被转换为Fish。结果与前面的 Java 示例完全相同(Kotlin 中调用操作符的实例与 Java 中相同)。因此,我们可以安全地调用isHungry方法,而无需进行任何显式转换。请注意,在这种情况下,此智能转换的范围受到if块的限制:

    if(animal is Fish) { 
        animal.isHungry() //1 
    } 

    animal.isHungry() //2, Error 
  1. 在这种情况下,animal 实例是 Fish,所以我们可以调用isHungry方法。

  2. 在这种情况下,animal 实例仍然是 Animal,所以我们无法调用isHungry方法。

然而,还有其他情况,智能转换范围大于单个块,就像以下示例中一样:

    val fish:Fish? = // ... 
    if (animal !is Fish) //1 
        return 

    animal.isHungry() //1 
  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 知道animalFish(智能转换)。

非空智能转换

智能转换还处理其他情况,包括空值检查。假设我们有一个标记为可空的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() 

通常(只要可能),底层类型如IntLongChar都经过优化(存储为原始类型),但我们仍然可以像在任何其他对象上一样调用它们的方法。

默认情况下,Java 平台将数字存储为 JVM 原始类型,但当需要可空数字引用(例如Int?)或涉及泛型时,Java 使用装箱表示装箱意味着将原始类型包装成相应的装箱原始类型。这意味着实例的行为就像一个对象。Java 原始类型的装箱表示的示例是int 与 Integerlong 与 Long。由于 Kotlin 编译为 JVM 字节码,这里也是如此:

    var weight: Int = 12 // 1 
    var weight: Int? = null // 2 
  1. 值以原始类型存储

  2. 值以装箱整数(复合类型)存储

这意味着每次我们创建一个数字(ByteShortIntLongDoubleFloat),或者使用CharBoolean,它将被存储为原始类型,除非我们将其声明为可空类型(Byte?Char?Array?等);否则,它将被存储为装箱表示:

    var a: Int = 1 // 1 
    var b: Int? = null // 2 
    b = 12 // 3 
  1. a是非空的,因此它以原始类型存储

  2. b为空,因此它被存储为装箱表示

  3. b仍然以装箱表示存储,尽管它有一个值

无法使用原始类型对泛型类型进行参数化,因此将执行装箱。重要的是要记住,使用装箱表示(复合类型)而不是主要表示可能会导致性能损失,因为它将始终创建与原始类型表示相比的内存开销。对于包含大量元素的列表和数组,这可能是显而易见的,因此对于应用程序性能来说,使用主要表示可能至关重要。另一方面,当涉及单个变量甚至多个变量声明时,我们不必担心表示的类型,即使在内存有限的 Android 世界中也是如此。

现在让我们讨论最重要的 Kotlin 原始数据类型:数字、字符、布尔值和数组。

数字

用于数字的基本 Kotlin 数据类型等效于 Java 数值原语:

然而,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 
  1. 定义类型为Char的变量

  2. 定义类型为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的数组。如果我们想要一个包含ShortLong的数组,那么我们必须明确指定数组类型:

    val array2: Array<Short> = arrayOf(1,2,3) 
    val array3: Array<Long> = arrayOf(1,2,3) 

如前所述,使用装箱表示可能会降低应用程序的性能。这就是为什么 Kotlin 有一些专门表示原始类型数组的类,以减少装箱内存开销:ShortArrayIntArrayLongArray等。这些类与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 
  1. 泛型数组的装箱长元素(推断类型:Array<Long>

  2. 包含原始长元素的数组(推断类型: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 中的数组是不变的。我们将在第六章,泛型是你的朋友中讨论变异

布尔类型

布尔是一个逻辑类型,有两个可能的值:truefalse。我们还可以使用可空布尔类型:

    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 
  1. 推断类型是IntRange(相当于i >= 1 && i <= 4

  2. 推断类型是CharRange(相当于从'b''g'的字母)

请注意,我们使用单引号来定义字符范围。

IntLongChar类型的范围可用于在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 更广泛地使用表达式,因此了解语句表达式之间的区别很重要。程序基本上是一系列语句和表达式。表达式产生一个值,可以作为另一个表达式、变量赋值或函数参数的一部分使用。表达式是一个或多个操作数(被操作的数据)和零个或多个操作符(表示特定操作的标记)的序列,可以评估为单个值。

让我们回顾一些来自 Kotlin 的表达式的例子:

表达式(产生一个值) 分配的值 类型的表达式
a = true true 布尔值
a = "foo" + "bar" "foobar" 字符串
a = min(2, 3) 2 整数
a = computePosition().getX() getX方法返回的值 整数

另一方面,语句执行一个动作,不能赋值给变量,因为它们根本没有值。语句可以包含用于定义类(class),接口(interface),变量(valvar),函数(fun),循环逻辑(breakcontinue)等的语言关键字。当表达式返回的值被忽略时(不将值赋给变量,不从函数中返回它,不将其用作其他表达式的一部分等),表达式也可以被视为语句。

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 > 10true时,这个表达式将返回第一个分支(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 中,循环可以迭代任何提供迭代器的内容。迭代器是一个具有两个方法的接口:hasNextnext。它知道如何迭代集合、范围、字符串或任何可以表示为元素序列的实体。

要迭代某些内容,我们必须提供一个 iterator() 方法。由于 String 没有这个方法,因此在 Kotlin 中它被定义为一个扩展函数。扩展将在第七章中介绍,即扩展函数和属性

Kotlin 提供了三种类型的循环:forwhiledo... 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) 

whiledo... while循环之间的主要区别在于条件表达式的评估时间。while循环在代码执行之前检查条件,如果条件不为真,则代码不会被执行。另一方面,do... while循环首先执行循环体,然后评估条件表达式,因此循环体至少会执行一次。如果这个表达式为true,循环将重复。否则,循环终止。

其他迭代

还有其他使用内置标准库函数进行集合迭代的方法,比如forEach。我们将在第七章,扩展函数和属性中介绍它们。

Break and continue

Kotlin 中的所有循环都支持经典的breakcontinue语句。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 

breakcontinue语句在处理嵌套循环时特别有用。它们可以简化我们的控制流程,并显著减少执行的工作量,以节省宝贵的 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时,我们跳过外部循环的迭代。

continuebreak语句都在封闭循环上执行相应的操作。然而,有时我们希望从一个循环中终止或跳过另一个循环的迭代;例如,从内部循环中终止外部循环的迭代:

    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是标签名称。按照惯例,标签名称始终以*@*开头,后跟标签名称。标签放置在循环之前。给循环加标签允许我们使用有资格的breakbreak@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块来捕获异常。默认情况下,异常会一直传递到调用堆栈的最上层,因此我们可以决定在哪里捕获它们。如果我们忘记捕获它们,它们将一直传递到调用堆栈的最上层,并以适当的消息停止线程执行(因此它们会提醒我们):

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 
        } 
    } 
  1. 异常没有像 Java 那样在函数签名上指定。

  2. 我们检查数据的有效性,并抛出NullPointerException(请注意,在创建对象实例时不需要使用 new 关键字)。

  3. try... catch块是 Java 中的类似结构。

  4. 仅处理特定的异常(AssertionError异常)。

  5. finally块总是被执行。

可能有零个或多个catch块,finally块可以省略。但是,至少应该有一个catchfinally块存在。

在 Kotlin 中,异常处理try是一个表达式,因此它可以返回一个值,我们可以将其值分配给一个变量。实际分配的值是执行块的最后一个表达式。让我们检查设备上是否安装了特定的 Android 应用程序:

val result = try { // 1 
    context.packageManager.getPackageInfo("com.text.app", 0)  //2 
    true 
} catch (ex: PackageManager.NameNotFoundException) { // 3 
    false 
} 
  1. try... catch块返回的值是由单个表达式函数返回的值。

  2. 如果应用程序已安装,getPackageInfo方法将返回一个值(此值被忽略),并且将执行包含true表达式的下一行。这是try块执行的最后一个操作,因此它的值将被分配给一个变量(true)。

如果应用程序未安装,getPackageInfo将抛出PackageManager.NameNotFoundException,并且将执行catch块的最后一行,其中包含一个false表达式,因此它的值将被分配给一个变量。

编译时常量

由于val变量是只读的,在大多数情况下我们可以将其视为常量。我们需要意识到它的初始化可能会延迟,这意味着有些情况下val变量可能在编译时未被初始化,例如,将方法调用的结果赋给一个值:

   val fruit:String  = getName() 

这个值将在运行时被赋值。然而,有些情况下我们需要在编译时知道这个值。当我们想要向注解传递参数时,需要确切的值。注解是由注解处理器处理的,它在应用程序启动之前就运行了:

为了确保值在编译时是已知的(因此可以被注解处理器处理),我们需要用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! 
    } 
  1. 函数定义了单个参数 args,其中包含用于运行程序的所有参数的数组(从命令行)。它被定义为非空,因为在没有任何参数的情况下启动程序时,会将空数组传递给方法。

  2. 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) 
    } 
  1. 与 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 
    }  
  1. 没有指定返回类型,因此返回类型隐式设置为 Unit。

  2. 我们可以只使用没有任何值的返回。

  3. 当函数返回 Unit 时,返回调用是可选的。我们不必使用它。

我们也可以使用返回Unit,但不应该使用,因为那样会误导并且不易读。

当我们指定返回类型时,除了Unit,我们总是需要显式返回值:

    fun sumPositive(a: Int, b: Int): Int { 
        if(a > 0 && b > 0) { 
            return a + b 
        } 
        // Error, 1 
    } 
  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,因此将使用这种类型。例如,这是具有类型Stringvararg参数的类型:

    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 
  1. 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函数,并将其定义为单表达式函数:

正如我们所看到的,单表达式函数具有表达式体而不是块体。这种表示法更短,但整个体必须只是一个单一表达式。

在单表达式函数中,声明返回类型是可选的,因为它可以从表达式的类型中推断出来。这就是为什么我们可以简化square函数,并以这种方式定义它:

    fun square(x: Int) = x * x 

在 Android 应用程序中有许多地方可以利用单表达式函数。让我们考虑一下提供布局 ID 并创建ViewHolderRecyclerView适配器:

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 
} 
  1. 不需要类型,因为它是从 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

  • maxOfminOf

  • 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 
        } 
    } 
  1. PrinterKt是由文件名和*Kt*后缀构成的名称。

  2. 所有顶层函数和属性都被编译为静态方法和变量。

  3. 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 允许在许多上下文中定义函数。我们可以在顶层定义函数,作为成员(在classinterface等内部),以及在其他函数内部(局部函数)。考虑以下局部函数定义的示例:

    fun printTwoThreeTimes() { 
        fun printThree() { // 1 
            print(3) 
        } 
        printThree() // 2 
        printThree() // 2 
    } 
  1. printThree是一个局部函数,因为它位于另一个函数内部。

  2. 局部函数无法从其声明的函数外部访问。

局部函数内可访问的元素不必从封闭函数作为参数传递,因为它们可以直接访问。例如:

    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") 
        } 
    } 
  1. 局部函数可以访问封闭函数内定义的注释参数和局部变量(下载和 ID)。

如果我们想将printLog定义为顶层函数,那么我们必须将idsdownloaded作为参数传递:

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函数)。

  • 在单元测试中用于抛出错误的函数。当我们需要测试代码中的错误处理时,这是很有用的。

对于这种情况,有一个特殊的类叫做NothingNothing类是空类型(无人居住类型),意味着它没有实例。具有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被称为空类型,这意味着在运行时没有值可以具有这种类型,它也是每个其他类的子类型。

无人居住类型的概念在 Java 世界中是新的,这就是为什么它可能令人困惑。这个想法实际上非常简单。Nothing实例从未存在,只有可能从指定它为返回类型的函数中返回的错误。而且没有必要将Nothing添加到某些东西中以影响其类型。

总结

在本章中,我们学习了如何定义和使用函数。我们了解了如何在顶层或其他函数内定义函数。还讨论了与函数相关的不同特性——可变参数、默认名称和命名参数语法。最后,我们看到了一些 Kotlin 特殊的返回类型:Unit,它相当于 Java 的void,以及Nothing,它是一种无法定义的类型,意味着什么也不能返回(只能抛出异常)。

在下一章中,我们将看到 Kotlin 中如何定义类。类在 Kotlin 语言中也得到了特别的支持,并且引入了许多改进,超过了 Java 的定义。

第四章:类和对象

Kotlin 语言为面向对象编程提供了全面的支持。我们将审查强大的结构,这些结构使我们能够简化数据模型的定义,并以一种简单灵活的方式对其进行操作。我们将了解 Kotlin 如何简化和改进许多从 Java 中已知的概念的实现。我们将看看不同类型的类、属性、初始化块和构造函数。我们将学习运算符重载和接口默认实现。

在本章中,我们将涵盖以下主题:

  • 类声明

  • 属性

  • 属性访问语法

  • 构造函数和初始化块

  • 构造函数

  • 继承

  • 接口

  • 数据类

  • 破坏性声明

  • 运算符重载

  • 对象声明

  • 对象表达式

  • 伴生对象

  • 枚举类

  • 密封类

  • 嵌套类

类是面向对象编程的基本构建块。事实上,Kotlin 类与 Java 类非常相似。然而,Kotlin 允许更多的功能,以及更简单和更简洁的语法。

类声明

在 Kotlin 中,使用class关键字定义类。以下是最简单的类声明--一个名为Person的空类:

    class Person 

Person的定义不包含任何主体。尽管如此,它可以使用默认构造函数进行实例化:

    val person = Person() 

即使是类实例化这样简单的任务在 Kotlin 中也得到了简化。与 Java 不同,Kotlin 不需要new关键字来创建类实例。由于 Kotlin 与 Java 的强大互操作性,我们可以以完全相同的方式实例化在 Java 和 Kotlin 中定义的类(不需要new关键字)。用于实例化类的语法取决于创建类实例的实际语言(Kotlin 或 Java),而不是声明类的语言:

    // Instantiate Kotlin class inside Java file 
    Person person = new Person() 

    // Instantiate class inside Kotlin file 
    var person = Person() 

在 Java 文件中使用new关键字而在 Kotlin 文件中永远不要使用new关键字是一个经验法则。

属性

属性只是支持字段及其访问器的组合。它可以是具有 getter 和 setter 的支持字段,也可以是只有其中一个的支持字段。属性可以在顶层(直接在文件内部)或作为成员(例如,在类、接口等内部)进行定义。

一般来说,建议定义属性(具有 getter/setter 的私有字段)而不是直接访问公共字段(根据Effective Java, by Joshua Bloch,书中的第 14 项建议:在公共类中,使用访问器方法,而不是公共字段)。

**Java 私有字段的 getter 和 setter 约定

Getter**:一个无参数方法,其名称对应于属性名称,并带有get前缀(对于Boolean属性,可能会使用is前缀)

Setter:以set开头的单参数方法:例如,setResult(String resultCode)

Kotlin 通过语言设计来保护这一原则,因为这种方法提供了各种封装的好处:

  • 能够在不改变外部 API 的情况下改变内部实现

  • 强制不变量(调用验证对象状态的方法)

  • 能够在访问成员时执行附加操作(例如,记录操作)

要定义顶级属性,我们只需在 Kotlin 文件中定义它:

    //Test.kt 
    val name:String  

假设我们需要一个类来存储有关人员的基本数据。这些数据可以从外部 API(后端)下载,也可以从本地数据库中检索。我们的类将需要定义两个(成员)属性,nameage。让我们先看一下 Java 的实现:

    public class Person { 

        private int age; 
        private String name; 

        public Person(String name, int age) { 
            this.name = name; 
            this.age = age; 
        } 

        public int getAge() { 
            return age; 
        } 

        public void setAge(int age) { 
            this.age = age; 
        } 

        public String getName() { 
            return name; 
        } 

        public void setName(String name) { 
            this.name = name; 
        } 
    } 

这个类只包含两个属性。由于我们可以让 Java IDE 为我们生成访问器代码,至少我们不必自己编写代码。然而,这种方法的问题在于我们无法摆脱这些自动生成的代码块,这使得代码非常冗长。我们(开发人员)大部分时间都在阅读代码,而不是编写代码,因此阅读冗余的代码浪费了大量宝贵的时间。而且,像重构属性名称这样的简单任务变得有点棘手,因为 IDE 可能不会更新构造函数参数名称。

幸运的是,使用 Kotlin 可以显著减少样板代码。Kotlin 通过引入内置于语言中的属性的概念来解决这个问题。让我们看一下前面的 Java 类的 Kotlin 等价物:

    class Person { 
        var name: String 
        var age: Int 

        constructor(name: String, age: Int) { 
            this.name = name 
            this.age = age 
        } 
    } 

这是前面的 Java 类的确切等价物:

  • constructor方法相当于在创建对象实例时调用的 Java 构造函数

  • Kotlin 编译器生成 getter 和 setter

我们仍然可以定义 getter 和 setter 的自定义实现。我们将在自定义 getter/setter 部分中更详细地讨论这一点。

我们已经定义的所有构造函数都被称为次要构造函数。Kotlin 还提供了另一种非常简洁的语法来定义构造函数。我们可以将构造函数(带有所有参数)定义为类头的一部分。这种类型的构造函数被称为主构造函数。让我们将属性声明从次要构造函数移动到主构造函数,以使我们的代码变得更短:

    class Person constructor(name: String, age: Int) { 
        var name: String 
        var age: Int 

        init { 
            this.name = name 
            this.age = age 
            println("Person instance created") 
        } 
    } 

在 Kotlin 中,与次要构造函数相反,主构造函数不能包含任何代码,因此所有初始化代码必须放在初始化块(init)中。初始化块将在类创建期间执行,因此我们可以在其中将构造函数参数赋值给字段。

为了简化代码,我们可以删除初始化块,并直接在属性初始化程序中访问构造函数参数。这使我们能够将构造函数参数赋值给字段:

    class Person constructor(name: String, age: Int) { 
        var name: String = name 
        var age: Int = age 
    } 

我们设法使代码变得更短,但仍然包含大量样板,因为类型声明和属性名称被重复(构造函数参数、字段赋值和字段本身)。当属性没有任何自定义的 getter 或 setter 时,我们可以通过在主构造函数中直接添加 val 或 var 修饰符来定义它们:

    class Person constructor (var name: String, var age: Int) 

最后,如果主构造函数没有任何注解(@Inject等)或可见性修饰符(publicprivate等),那么可以省略constructor关键字:

    class Person (var name: String, var age: Int)

当构造函数接受几个参数时,最好将每个参数定义在新的一行中,以提高代码可读性并减少潜在合并冲突的机会(当从源代码存储库合并分支时):

    class Person( 
        var name: String,  
        var age: Int 
    ) 

总之,前面的例子等同于本节开头呈现的 Java 类--两个属性都直接在类主构造函数中定义,Kotlin 编译器为我们做了所有工作--它生成了适当的字段和访问器(getter/setter)。

请注意,此表示法仅包含有关此数据模型类的最重要信息--其名称、参数名称、类型和可变性(val/var)信息。实现几乎没有样板。这使得类非常易于阅读、理解和维护。

读写属性与只读属性

在前面的例子中,所有属性都被定义为读写(生成了 setter 和 getter)。要定义只读属性,我们需要使用val关键字,这样只会生成 getter。让我们看一个简单的例子:

    class Person( 
        var name: String, 

        // Read-write property (generated getter and setter)

        val age: Int      // Read-only property (generated getter) 
    ) 

    \\usage 
    val person = Person("Eva", 25) 

    val name = person.name 
    person.name = "Kate" 

    val age = person.age 
    person.age = 28 \\error: read-only property 

Kotlin 不支持只写属性(只生成 setter 的属性)。

关键字
var
val
(不支持)

Kotlin 和 Java 之间的属性访问语法

Kotlin 引入的另一个重大改进是访问属性的方式。在 Java 中,我们会使用相应的方法(setSpeed/getSpeed)来访问属性。Kotlin 提倡属性访问语法,这是一种更具表现力的访问属性的方式。让我们比较这两种方法,假设我们有一个简单的Car类,它有一个名为speed的属性:

    class Car (var speed: Double) 

    //Java access properties using method access syntax 
    Car car = new Car(7.4) 
    car.setSpeed(9.2) 
    Double speed = car.getSpeed(); 

    //Kotlin access properties using property access syntax 
    val car: Car = Car(7.4) 
    car.speed = 9.2 
    val speed = car.speed 

正如我们所看到的,在 Kotlin 中,访问或修改对象属性时不需要添加getset前缀和括号。使用属性访问语法允许直接使用增量(++)和减量(--)运算符与属性访问一起使用:

    val car = Car(7.0) 
    println(car.speed)  //prints 7.0 
    car.speed++ 
    println(car.speed)  //prints 8.0 
    car.speed-- 
    car.speed-- 
    println(car.speed) //prints: 6.0 

增量和减量运算符

有两种类型的增量(++)和减量(--)运算符:前增量/前减量,其中运算符在表达式之前定义,和后增量/后减量,其中运算符在表达式之后定义:

    ++speed //pre increment

    --speed //pre decrement 

    speed++ //post increment 

    speed-- //post decrement

在前面的例子中,使用后增量/减量与前增量/减量不会改变任何东西,因为这些操作是按顺序执行的。但是当增量/减量运算符与函数调用结合时,这将产生很大的差异。

在预增量运算符中,速度被检索,增加,并作为参数传递给函数:

    var speed = 1.0 
    println(++speed) // Prints: 2.0   
    println(speed)   // Prints: 2.0 

在后增量运算符中,速度被检索,作为参数传递给函数,然后增加,所以旧值被传递给函数:

    var speed = 1.0 
    println(speed++) // Prints: 1.0 
    println(speed) // Prints: 2.0 

这对于预减量和后减量运算符也是类似的。

属性访问语法不仅限于在 Kotlin 中定义的类。遵循 Java 约定的 getter 和 setter 的每个方法在 Kotlin 中表示为属性。

这意味着我们可以在 Java 中定义一个类,并使用属性访问语法在 Kotlin 中访问其属性。让我们定义一个 Java Fish类,有两个属性,sizeisHungry,然后在 Kotlin 中实例化这个类并访问这些属性:

    //Java class declaration 
    public class Fish { 
        private int size; 
        private boolean hungry; 

        public Fish(int size, boolean isHungry) { 
            this.size = size; 
            this.hungry = isHungry; 
        } 

        public int getSize() { 
            return size; 
        } 

        public void setSize(int size) { 
            this.size = size; 
        } 

        public boolean isHungry() { 
            return hungry; 
        } 

        public void setHungry(boolean hungry) { 
            this.hungry = hungry; 
        } 
    } 

    //Kotlin class usage 
    val fish = Fish(12, true) 
    fish.size = 7 
    println(fish.size) // Prints: 7 
    fish.isHungry = true 
    println(fish.isHungry) // Prints: true 

这两种方式都可以,所以我们可以使用非常简洁的语法在 Kotlin 中定义Fish类,并以通常的 Java 方式访问它,因为 Kotlin 编译器将生成所有必需的 getter 和 setter:

    //Kotlin class declaration 
    class Fish(var size: Int, var hungry: Boolean) 

    //class usage in Java 
    Fish fish = new Fish(12, true); 
    fish.setSize(7); 
    System.out.println(fish.getSize()); 
    fish.setHungry(false); 
    System.out.println(fish.getHungry()); 

正如我们所看到的,用于访问类属性的语法取决于类使用的实际语言,而不是声明类的语言。这允许更多地使用在 Android 框架中定义的许多类的习惯用法。让我们看一些例子:

Java 方法访问语法 Kotlin 属性访问语法
activity.getFragmentManager() activity.fragmentManager
view.setVisibility(Visibility.GONE) view.visibility = Visibility.GONE
context.getResources().getDisplayMetrics().density context.resources.displayMetrics.density

属性访问语法导致更简洁的代码,减少了原始 Java 语言的复杂性。请注意,虽然在 Kotlin 中仍然可以使用方法访问语法,但属性访问语法通常是更好的选择。

在 Android 框架中有一些方法的名称使用is前缀;在这种情况下,布尔属性也有is前缀:

    class MainActivity : AppCompatActivity() { 

        override fun onDestroy() { // 1 
            super.onDestroy() 

            isFinishing() // method access syntax 
            isFinishing // property access syntax 
            finishing // error 
        } 
    } 
  1. Kotlin 使用override修饰符标记重写的成员,而不是像 Java 那样使用@Override注解。

尽管使用finishing可能是最自然和一致的方法,但由于潜在的冲突,无法默认使用它。

另一种情况下,我们无法使用属性访问语法的是当属性只定义了 setter 而没有 getter 时,因为 Kotlin 不支持只写属性,就像这个例子:

    fragment.setHasOptionsMenu(true) 
    fragment.hasOptionsMenu = true // Error!

自定义 getter/setter

有时我们希望对属性的使用有更多的控制。我们可能希望在使用属性时执行其他辅助操作;例如,在将值分配给字段之前验证值,记录整个操作,或使实例状态无效。我们可以通过指定自定义 setter 和/或 getter 来实现。让我们将ecoRating属性添加到我们的Fruit类中。在大多数情况下,我们会像这样将此属性添加到类声明头中:

    class Fruit(var weight: Double, 

                val fresh: Boolean, 

                val ecoRating: Int) 

如果我们想定义自定义的 getter 和 setter,我们需要在类体中定义属性,而不是在类声明头中。让我们将ecoRating属性移到类主体中:

class Fruit(var weight: Double, val fresh: Boolean, ecoRating: Int)     

{ 
    var ecoRating: Int = ecoRating  
} 

当属性在类的主体内部定义时,我们必须用值初始化它(即使可空属性也需要用空值初始化)。我们可以提供默认值,而不是用构造函数参数填充属性:

    class Fruit(var weight: Double, val fresh: Boolean) { 
        var ecoRating: Int = 3 
    } 

我们还可以基于其他属性计算默认值:

    class Apple(var weight: Double, val fresh: Boolean) { 
        var ecoRating: Int = when(weight) { 
            in 0.5..2.0 -> 5 
            in 0.4..0.5 -> 4 
            in 0.3..0.4 -> 3 
            in 0.2..0.3 -> 2 
            else -> 1 
        } 
    } 

不同的值将为不同的权重构造函数参数设置。

当属性在类体中定义时,类型声明可以省略,因为它可以从上下文中推断出来:

    class Fruit(var weight: Double) { 
        var ecoRating = 3 
    } 

让我们定义一个具有默认行为的自定义 getter 和 setter,该行为将等同于前面的属性:

    class Fruit(var weight: Double) { 
        var ecoRating: Int = 3 
        get() { 
            println("getter value retrieved") 
            return field 
        } 
        set(value) { 
            field = if (value < 0) 0 else value 
            println("setter new value assigned $field") 
        } 
    } 

    // Usage 
    val fruit = Fruit(12.0) 
    val ecoRating = fruit.ecoRating 

    // Prints: getter value retrieved 
    fruit.ecoRating = 3;        

    // Prints: setter new value assigned 3 
    fruit.ecoRating = -5;       

    // Prints: setter new value assigned 0 

getset块内,我们可以访问一个名为field的特殊变量,它指的是属性的对应后备字段。请注意,Kotlin 属性声明与自定义 getter/setter 紧密相关。这与 Java 相矛盾,并解决了字段声明通常位于包含类和相应 getter/setter 的文件顶部的问题,因此我们无法在单个屏幕上看到它们,因此代码更难阅读。除了位置之外,Kotlin 属性行为与 Java 非常相似。每次从ecoRating属性中检索值时,都会执行get块,每次将新值分配给ecoRating属性时,都会执行set块。

这是一个读写属性(var),因此它可能包含相应的 getter 和 setter。如果我们只显式定义其中一个,另一个将使用默认实现。

要使属性值在检索属性值时每次计算,我们需要显式定义 getter:

    class Fruit(var weight: Double) { 
        val heavy             // 1 
        get() = weight > 20  
    } 

    //usage 
    var fruit = Fruit(7.0) 
    println(fruit.heavy) //prints: false 
    fruit.weight = 30.5 
    println(fruit.heavy) //prints: true 
  1. 自 Kotlin 1.1 开始,类型可以省略(它将被推断)。

getter 与属性默认值

在前面的示例中,我们使用了 getter,因此每次检索值时都会计算属性值。通过省略 getter,我们可以为属性创建默认值。这个值将在类创建期间仅计算一次,永远不会改变(改变weight属性不会影响isHeavy属性的值):

    class Fruit(var weight: Double) { 
        val isHeavy = weight > 20 
    } 

    var fruit = Fruit(7.0) 
    println(fruit.isHeavy) // Prints: false 
    fruit.weight = 30.5 
    println(fruit.isHeavy) // Prints: false 

这种类型的属性有后备字段,因为它的值始终在对象创建期间计算。我们还可以创建没有后备字段的读写属性:

    class Car { 
        var usable: Boolean = true 
        var inGoodState: Boolean = true 

       var crashed: Boolean 
       get() = !usable && !inGoodState 
       set(value) { 
           usable = false 
           inGoodState = false 
       } 
    } 

这种类型的属性没有后备字段,因为它的值始终使用另一个属性计算。

延迟初始化属性

有时我们知道属性不会为空,但它不会在声明时初始化值。让我们看看常见的 Android 示例--检索布局元素的引用:

    class MainActivity : AppCompatActivity() { 

       private var button: Button? = null 

       override fun onCreate(savedInstanceState: Bundle?) { 
           super.onCreate(savedInstanceState) 
           button = findViewById(R.id.button) as Button 
       } 
    } 

button变量不能在声明时初始化,因为MainActivity布局尚未初始化。我们可以在onCreate方法中检索定义在布局中的按钮的引用,但为了做到这一点,我们需要将变量声明为可空(Button?)。

这种方法似乎相当不实用,因为在调用onCreate方法后,button实例始终可用。然而,客户端仍然需要使用安全调用运算符或其他空值检查来访问它。

为了避免在访问属性时进行空值检查,我们需要一种方法来告诉 Kotlin 编译器,这个变量将在使用之前填充,但它的初始化将被延迟。为此,我们可以使用lateinit修饰符:

    class MainActivity : AppCompatActivity() { 

        private lateinit var button: Button 

        override fun onCreate(savedInstanceState: Bundle?) { 
            button = findViewById(R.id.button) as Button 
            button.text = "Click Me" 
        } 
    } 

现在,通过将属性标记为lateinit,我们可以在不执行空值检查的情况下访问我们的应用程序实例。

lateinit修饰符告诉编译器,这个属性是非空的,但它的初始化被延迟了。当我们在初始化之前尝试访问属性时,应用程序会抛出UninitializedPropertyAccessException。这没关系,因为我们假设这种情况不应该发生。

在声明时无法初始化变量的情况非常普遍,而且并不总是与视图有关。属性可以通过依赖注入或单元测试的setup方法进行初始化。在这种情况下,我们无法在构造函数中提供非空值,但仍希望避免空值检查。

lateinit 属性和框架

lateinit属性在属性由依赖注入框架注入时也很有帮助。流行的 Android 依赖注入框架Dagger使用@Inject注解来标记需要注入的属性:

@Inject lateinit var locationManager: LocationManager

我们知道属性永远不会为空(因为它将被注入),但 Kotlin 编译器不理解这个注释。

类似的情况也发生在流行的框架Mockito中:@Mock lateinit var mockEventBus: EventBus

变量将被模拟,但这将在测试类创建后的某个时候发生。

属性注释

Kotlin 从单个属性生成多个 JVM 字节码元素(private字段,getter,setter)。有时框架注解处理器或基于反射的库需要将特定元素定义为公共字段。这种行为的一个很好的例子是 JUnit 测试框架。它要求通过测试类字段或 getter 方法提供规则。当定义ActivityTestRule或 Mockito 的(用于单元测试的模拟框架)Rule注解时,我们可能会遇到这个问题:

    @Rule 
    val activityRule = ActivityTestRule(MainActivity::class.Java) 

前面的代码注释了 JUnit 无法识别的 Kotlin 属性,因此无法正确初始化ActivityTestRule。JUnit 注解处理器期望在字段或 getter 上有Rule注解。有几种方法可以解决这个问题。我们可以通过使用@JvmField注释将 Kotlin 属性公开为 Java 字段:

    @JvmField @Rule 
    val activityRule = ActivityTestRule(MainActivity::class.Java) 

该字段将具有与基础属性相同的可见性。关于@JvmField注释的使用有一些限制。如果属性具有支持字段,不是私有的,没有 open,override 或 const 修饰符,并且不是委托属性,我们可以使用@JvmField注释属性。

我们还可以通过直接向 getter 添加注释来注释 getter:

    val activityRule 
    @Rule get() = ActivityTestRule(MainActivity::class.java) 

如果我们不想定义 getter,我们仍然可以使用使用地点目标(get)向 getter 添加注释。通过这样做,我们只需指定由 Kotlin 编译器生成的哪个元素将被注释:

    @get:Rule  
    val activityRule = ActivityTestRule(MainActivity::class.Java) 

内联属性

我们可以通过使用inline修饰符来优化属性调用。在编译期间,每个属性调用都将被优化。调用属性时,调用将被替换为属性体:

    inline val now: Long  
        get() { 
            println("Time retrieved") 
            return System.currentTimeMillis() 
        } 

使用内联属性时,我们使用inline修饰符。前面的代码将被编译为:

    println("Time retrieved") 
    System.currentTimeMillis() 

内联可以提高性能,因为不需要创建额外的对象。不会调用 getter,因为体会替换属性的使用。内联有一个限制——它只能应用于没有支持字段的属性。

构造函数

Kotlin 允许我们定义没有任何构造函数的类。我们还可以定义一个主构造函数和一个或多个次要构造函数:

    class Fruit(val weight: Int) { 
        constructor(weight: Int, fresh: Boolean) : this(weight) { } 
    } 

    //class instantiation 
    val fruit1 = Fruit(10) 
    val fruit2 = Fruit(10, true) 

不允许为次要构造函数声明属性。如果我们需要一个由次要构造函数初始化的属性,我们必须在类体中声明它,并且可以在次要构造函数体中初始化它。让我们定义fresh属性:

    class Test(val weight: Int) { 
        var fresh: Boolean? = null 

        //define fresh property in class body 

        constructor(weight: Int, fresh: Boolean) : this(weight) { 
            this.fresh = fresh 

            //assign constructor parameter to fresh property 
        } 
    } 

请注意,我们将fresh属性定义为可空,因为当使用主构造函数创建对象实例时,fresh属性将为 null:

    val fruit = Fruit(10) 
    println(fruit.weight) // prints: 10 
    println(fruit.fresh) // prints: null 

我们还可以为fresh属性分配默认值,使其成为非空:

    class Fruit(val weight: Int) { 
        var fresh: Boolean = true 

        constructor(weight: Int, fresh: Boolean) : this(weight) { 
            this.fresh = fresh 
        } 
    } 

    val fruit = Fruit(10) 
    println(fruit.weight) // prints: 10 
    println(fruit.fresh) // prints: true 

当定义主构造函数时,每个次要构造函数都必须隐式或显式调用主构造函数。隐式调用意味着我们直接调用主构造函数。显式调用意味着我们调用另一个调用主构造函数的次要构造函数。要调用另一个构造函数,我们使用this关键字:

class Fruit(val weight: Int) { 

    constructor(weight: Int, fresh: Boolean) : this(weight) // 1 

    constructor(weight: Int, fresh: Boolean, color: String) : 

                this(weight, fresh) // 2 
} 
  1. 调用主构造函数

  2. 调用次要构造函数

如果类没有主构造函数,并且超类具有非空构造函数,则每个次要构造函数都必须使用super关键字初始化基类或调用另一个执行此操作的构造函数:

class ProductView : View { 
   constructor(ctx: Context) : super(ctx) 
   constructor(ctx: Context, attrs : AttributeSet) : 

               super(ctx, attrs) 
} 

通过使用@JvmOverloads注释,可以大大简化视图示例,该注释将在@JvmOverloads部分中描述。

默认情况下,此生成的构造函数将是公共的。如果我们想要阻止生成这样的隐式public构造函数,我们必须声明一个带有privateprotected可见性修饰符的空主构造函数:

    class Fruit private constructor()  

要更改构造函数的可见性,我们需要在类定义头部显式使用constructor关键字。当我们想要注释构造函数时,也需要constructor关键字。一个常见的例子是使用 Dagger(依赖注入框架)@Inject注释注释类构造函数:

    class Fruit @Inject constructor() 

可以同时应用可见性修饰符和注释:

    class Fruit @Inject private constructor { 
        var weight: Int? = null 
    } 

属性与构造函数参数

需要注意的重要事情是,如果从构造函数属性声明中删除var/val关键字,我们将得到一个构造函数参数声明。这意味着属性将被更改为构造函数参数,因此不会生成访问器,我们将无法在类实例上访问属性:

    class Fruit(var weight:Double, fresh:Boolean) 

    val fruit = Fruit(12.0, true) 
    println(fruit.weight) 
    println(fruit.fresh) // error 

在上面的例子中,我们有一个错误,因为fresh缺少valvar关键字,所以它是一个构造函数参数,而不是类属性,如weight。以下表总结了编译器访问器生成:

类声明 生成的 Getter 生成的 Setter 类型
class Fruit (name:String) 构造函数参数
class Fruit (val name:String) 属性
class Fruit (var name:String) 属性

有时我们可能会想知道何时应该使用属性,何时应该使用方法。遵循的一个好的指导原则是在以下情况下使用属性而不是方法:

  • 它不会抛出异常

  • 计算便宜(或在第一次运行时缓存)

  • 它在多次调用时返回相同的结果

带有默认参数的构造函数

自从 Java 早期以来,对象创建存在严重缺陷。当对象需要多个参数并且其中一些参数是可选时,很难创建对象实例。有几种方法可以解决这个问题,例如,Telescoping 构造函数模式,JavaBeans 模式,甚至 Builder 模式。它们各有利弊。

模式

这些模式解决了对象创建的问题。每个模式的解释如下:

  • Telescoping 构造函数模式:具有一系列构造函数的类,其中每个构造函数都添加一个新参数。现在被认为是一种反模式,但 Android 框架仍在一些地方使用它;例如,android.view.View类:
        val view1 = View(context) 
        val view1 = View(context, attributeSet) 
        val view1 = View(context, attributeSet, defStyleAttr) 
  • JavaBeans 模式:无参数构造函数加上一个或多个设置器方法来配置对象。这种模式的主要问题是我们无法确定对象上是否已调用了所有必需的方法,因此它可能只是部分构造的:
        val animal = Animal() 
        fruit.setWeight(10) 
        fruit.setSpeed(7.4) 
        fruit.setColor("Gray")
  • 构建器模式:使用另一个对象,即构建器,逐步接收初始化参数,然后在调用构建方法时一次性返回生成的对象;例如,android.app.Notification.Builderandroid.app.AlertDialog.Builder
        Retrofit retrofit = new Retrofit.Builder() 
                                .baseUrl("https://api.github.com/") 
                                .build();

长期以来,builder是最广泛使用的,但默认参数命名参数语法的组合是更简洁的选项。让我们定义一些默认值:

    class Fruit(weight: Int = 0, fresh: Boolean = true, color: 

                String = "Green") 

通过定义默认参数值,我们可以以多种方式创建对象,而无需传递所有参数:

    val fruit = Fruit(7.4, false) 
    println(fruit.fresh) // prints: false 

    val fruit2 = Fruit(7.4) 
    println(fruit.fresh) // prints: true 

使用带有默认参数的参数语法可以在对象创建时给我们更多的灵活性。我们可以按任何顺序传递所需的参数,而无需定义多个方法和构造函数,就像以下示例中所示:

val fruit1 = Fruit (weight = 7.4, fresh = true, color = "Yellow") 

val fruit2 = Fruit (color = "Yellow") 

继承

正如我们已经知道的那样,所有 Kotlin 类型的超类型都是Any。它相当于 Java 的Object类型。每个 Kotlin 类都显式或隐式地扩展了Any类。如果我们没有指定父类,Any将被隐式设置为类的父类:

    class Plant // Implicitly extends Any 
    class Plant : Any // Explicitly extends Any 

Kotlin 像 Java 一样,提倡单一继承,所以一个类只能有一个父类,但可以实现多个接口。

与 Java 相比,Kotlin 中的每个类和每个方法默认都是 final 的。这符合Effective Java Item 17: Design and document for inheritance or else prohibit it规则。这用于防止子类修改基类的意外行为。修改基类的代码可能会导致子类的不正确行为,因为基类的更改代码不再符合其子类的假设。

这意味着在使用open关键字显式声明之前,类和方法都不能被扩展或覆盖。这与 Java 的final关键字完全相反。

假设我们想声明一个基类Plant和子类Tree

    class Plant  
    class Tree : Plant() // Error 

前面的代码不会编译,因为Plant类默认是 final 的。让我们将其改为open

    open class Plant  
    class Tree : Plant() 

请注意,我们在 Kotlin 中通过使用冒号字符(:)来简单地定义继承。没有 Java 中的extendsimplements关键字。

现在让我们在Plant类中添加一些方法和属性,并尝试在Tree类中覆盖它:

    open class Plant { 
        var height: Int = 0 
        fun grow(height: Int) {} 
    } 

    class Tree : Plant() { 
        override fun grow(height: Int) { // Error 
            this.height += height 
        } 
    } 

这段代码也不会编译。我们已经说过,默认情况下所有方法也是关闭的,因此我们想要覆盖的每个方法都必须显式标记为open。让我们通过将grow方法标记为 open 来修复代码:

    open class Plant { 
        var height: Int = 0 
        open fun grow(height: Int) {} 
    } 

    class Tree : Plant() { 
        override fun grow(height: Int) { 
            this.height += height 
        } 
    } 

类似地,我们可以打开并覆盖height属性:

    open class Plant { 
        open var height: Int = 0 
        open fun grow(height: Int) {} 
    } 

    class Tree : Plant() { 
        override var height: Int = super.height 
            get() = super.height 
            set(value) { field = value} 

        override fun grow(height: Int) { 
            this.height += height 
        } 
    } 

要快速覆盖任何成员,转到声明成员的类,添加open修饰符,然后转到要覆盖成员的类,运行override成员(Windows 的快捷键是Ctrl + O,macOS 的快捷键是Command + O)操作,并选择要覆盖的所有成员。这样所有必需的代码将由 Android Studio 生成。

假设所有树都以相同的方式生长(相同的生长算法适用于所有树)。我们想允许创建Tree类的新子类以更好地控制树,但同时我们希望保留我们的生长算法--不允许Tree类的任何子类覆盖此行为。为了实现这一点,我们需要将Tree类中的grow方法显式标记为final

    open class Plant { 
        var height: Int = 0 

        open fun grow(height: Int) {} 
    } 

    class Tree : Plant() { 
        final override fun grow(height: Int) { 
            this.height += height 
        } 
    } 

    class Oak : Tree() { 
        // 1
    } 
  1. 这里不可能覆盖 grow 方法,因为它是final

让我们总结一下所有这些openfinal行为。为了使子类中的方法可以被重写,我们需要在父类中明确将其标记为open。为了确保重写的方法不会被任何子类再次重写,我们需要将其标记为final

在前面的例子中,Plant类中的 grow 方法实际上并没有提供任何功能(它的主体为空)。这表明也许我们根本不想实例化Plant类,而是将其视为基类,只实例化诸如扩展Plant类的Tree等各种类。我们应该将Plant类标记为abstract以禁止其实例化:

    abstract class Plant { 
        var height: Int = 0 

        abstract fun grow(height: Int) 
    } 

    class Tree : Plant() { 
        override fun grow(height: Int) { 
            this.height += height 
        } 
    } 
    val plant = Plant() 

    // error: abstract class can't be instantiated 
    val tree = Tree() 

将类标记为抽象也会使方法默认为开放状态,因此我们不必将每个成员显式标记为open。请注意,当我们将grow方法定义为抽象时,我们必须删除其主体,因为abstract方法不能有主体。

JvmOverloads 注解

Android 平台中的一些类使用 Telescoping 构造函数,这被认为是一种反模式。这样的类的一个很好的例子是android.view.View 类。可能会有一种情况,只使用一个构造函数(从 Kotlin 代码中膨胀自定义视图),但是在子类化子类android.view.View时,最好重写所有三个构造函数,因为类将在所有场景中正确工作。通常我们的自定义视图类会像这样:

    class CustomView : View { 

        constructor(context: Context?) : this(context, null) 

        constructor(context: Context?, attrs: AttributeSet?) : 

                    this(context, attrs, 0) 

        constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { 
            //... 
        } 
     } 

这种情况引入了大量样板代码,只是为了将构造函数委托给其他构造函数。Kotlin 解决这个问题的方法是使用@JvmOverload注解:

    class KotlinView @JvmOverloads constructor( 
        context: Context,  
        attrs: AttributeSet? = null,  
        defStyleAttr: Int = 0 
    ) : View(context, attrs, defStyleAttr) 

使用@JvmOverload注解注释构造函数会告诉编译器在 JVM 字节码中为每个具有默认值的参数生成额外的构造函数重载。在这种情况下,将生成所有必需的构造函数:

public SampleView(Context context) { 
    super(context); 
} 

public SampleView(Context context, @Nullable AttributeSet attrs) { 
    super(context, attrs); 
} 

public SampleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 
    super(context, attrs, defStyleAttr); 
} 

接口

Kotlin 接口类似于 Java 8 接口,与以前的 Java 版本的接口相反。使用interface关键字定义接口。让我们定义一个EmailProvider接口:

    interface EmailProvider { 
        fun validateEmail() 
   } 

要在 Kotlin 中实现前面的接口,使用与扩展类相同的语法--一个冒号字符(:)。与 Java 不同,没有implements关键字:

    class User:EmailProvider { 
        override fun validateEmail() { 
            //email validation 
        } 
    } 

可能会产生一个问题,如何同时扩展一个类并实现一个接口。只需在冒号后面放置类名,并使用逗号字符添加一个或多个接口。虽然不要求将超类放在第一个位置,但这被认为是一个良好的做法:

    open class Person {

        interface EmailProvider { 
            fun validateEmail() 
        } 

        class User: Person(), EmailProvider { 
        override fun validateEmail(){ 
            //email validation 
        } 
    } 

与 Java 一样,Kotlin 类只能扩展一个类,但可以实现一个或多个接口。我们还可以在接口中声明属性:

    interface EmailProvider { 
        val email: String 
        fun validateEmail() 
    } 

所有方法和属性都必须在实现接口的类中被重写:

    class User() : EmailProvider { 

        override val email: String = "UserEmailProvider" 

        override fun validateEmail() { 
            //email validation 
        } 
    } 

此外,可以使用主构造函数中定义的属性来重写接口中的参数:

    class User(override val email: String) : EmailProvider { 
        override fun validateEmail() { 
            //email validation 
        } 
    } 

在接口中定义的所有没有默认实现的方法和属性默认被视为抽象,因此我们不必显式地将它们定义为抽象。所有抽象方法和属性必须由实现接口的具体(非抽象)类实现(重写)。

然而,在接口中定义方法和属性的另一种方式。Kotlin 与 Java 8 类似,引入了对接口的重大改进。接口不仅可以定义行为,还可以实现它。这意味着接口可以提供默认的方法或属性实现。唯一的限制是接口不能引用任何后备字段--存储状态(因为没有好的存储位置)。这是接口和抽象类之间的不同因素。接口是无状态的(它们不能有状态),而抽象类是有状态的(它们可以有状态)。让我们看一个例子:

    interface EmailProvider { 

        fun validateEmail(): Boolean 

        val email: String 

        val nickname: String 
        get() = email.substringBefore("@") 
    } 
    class User(override val email: String) : EmailProvider { 
        override fun validateEmail() { 
            //email validation 
        } 
    } 

EmailProvider接口为nickname属性提供了默认实现,因此我们不必在User类中定义它,我们仍然可以像类中定义的任何其他属性一样使用该属性:

    val user = User (" johnny.bravo@test.com") 
    print(user.nickname) //prints: johnny 

方法也是一样。只需在接口中定义一个带有方法体的方法,因此User类将从接口中获取所有默认实现,并且只需要覆盖email成员--这是唯一一个没有默认实现的推断成员:

    interface EmailProvider { 

        val email: String 

        val nickname: String 
        get() = email.substringBefore("@") 

        fun validateEmail() = nickname.isNotEmpty() 
    } 

    class User(override val email: String) : EmailProvider 

    //usage 
    val user = User("joey@test.com") 
    print(user.validateEmail()) // Prints: true 
    print(user.nickname) // Prints: joey 

与默认实现相关的一个有趣的案例是,一个类不能继承自多个类,但可以实现多个接口。我们可以有两个包含具有相同签名和默认实现方法的接口:

    interface A { 
        fun foo() { 
            println("A") 
        } 
    } 

    interface B { 
        fun foo() { 
            println("B") 
        } 
    } 

在这种情况下,冲突必须通过在实现接口的类中覆盖foo方法来显式解决:

    class Item : A, B { 
        override fun foo() { 
            println("Item") 
        } 
    } 

    //usage 
    val item = Item() 
    item.foo() //prints: Item 

我们仍然可以通过使用尖括号限定super并指定父接口类型名称来调用默认接口实现:

    class Item : A, B { 
        override fun foo() { 
            val a = super<A>.foo() 
            val b = super<B>.foo() 
            print("Item $a $b") 
        } 
    } 

    //usage 
    val item = Item() 
    item.foo() 

    //Prints: A

              B

              ItemsAB

数据类

通常,我们创建一个唯一目的是存储数据的类;例如,从服务器或本地数据库检索的数据。这些类是应用程序数据模型的构建块:

    class Product(var name: String?, var price: Double?) { 

       override fun hashCode(): Int { 
           var result = if (name != null) name!!.hashCode() else 0 
           result = 31 * result + if (price != null) price!!.hashCode() 

           else 0 
           return result 
       } 

       override fun equals(other: Any?): Boolean = when { 
           this === other -> true 
           other == null || other !is Product -> false 
           if (name != null) name != other.name else other.name != 

                             null -> false 
           price != null -> price == other.price 
           else -> other.price == null 
       } 

       override fun toString(): String { 
           return "Product(name=$name, price=$price)" 
       } 
    } 

在 Java 中,我们需要生成大量冗余的 getter/setter 以及hashCodeequals方法。Android Studio 可以为我们生成大部分代码,但是维护这些代码仍然是一个问题。在 Kotlin 中,我们可以通过在类声明头部添加data关键字来定义一种特殊类型的类,称为数据类:

    class Product(var name: String, var price: Double) 

    // normal class

    data class Product(var name: String, var price: Double) 

    // data class

数据类通过 Kotlin 编译器生成的方法为类添加了额外的功能。这些方法包括equalshashCodetoStringcopy和多个componentN方法。限制是数据类不能标记为abstractinnersealed。让我们更详细地讨论数据修饰符添加的方法。

equals 和 hashCode 方法

在处理数据类时,通常需要比较两个实例的结构相等性(它们包含相同的数据,但不一定是相同的实例)。我们可能只是想检查User类的一个实例是否等于另一个User实例,或者两个产品实例是否代表相同的产品。用于检查对象是否相等的常见模式是使用一个使用hashCode方法内部的equals方法:

    product.equals(product2)  

对于重写hashCode的实现,通用约定是两个相等的对象(根据equals实现)需要具有相同的哈希码。背后的原因是hashCode经常在equals之前进行比较,因为它的性能更好--比较哈希码比对象中的每个字段要便宜得多。

如果hashCode相同,那么equals方法会检查两个对象是否是相同实例,相同类型,然后通过比较所有重要字段来验证它们是否相等。如果第一个对象的至少一个字段不等于第二个对象的相应字段,则这些对象不被视为相等。另一种情况是,当两个对象具有相同的hashCode并且所有重要(比较的)字段具有相同的值时,两个对象是相等的。让我们来看一个包含两个字段nameprice的 Java 产品类的例子:

    public class Product { 

        private String name; 
        private Double price; 

        public Product(String name, Double price) { 
            this.name = name; 
            this.price = price; 
        } 

        @Override 
        public int hashCode() { 
            int result = name != null ? name.hashCode() : 0; 
         result = 31 * result + (price != null ? 

                                 price.hashCode() : 0); 
            return result; 
    } 

        @Override 
        public boolean equals(Object o) { 
            if (this == o) { 
                return true; 
            } 
            if (o == null || getClass() != o.getClass()) { 
                return false; 
            } 

            Product product = (Product) o; 

            if (name != null ? !name.equals(product.name) : 

            product.name != null) { 
                return false; 
            } 
            return price != null ? price.equals(product.price) : 

            product.price == null; 
        } 

        public String getName() { 
            return name; 
        } 

        public void setName(String name) { 
            this.name = name; 
        } 

        public Double getPrice() { 
            return price; 
        } 

        public void setPrice(Double price) { 
            this.price = price; 
        } 
    } 

这种方法在 Java 和其他面向对象编程语言中被广泛使用。在早期,程序员们不得不为每个需要进行比较的类手动编写这段代码,并确保代码正确并比较每个重要的值。

如今,像 Android Studio 这样的现代 IDE 可以生成这段代码并更新适当的方法。我们不必编写代码,但我们仍然必须通过确保所有必需的字段通过equals方法进行比较来维护它。有时我们不知道这是否是 IDE 生成的标准代码,还是经过调整的版本。对于每个 Kotlin 数据类,这些方法都是由编译器自动生成的,因此这个问题不存在。以下是 Kotlin 中Product的定义,其中包含了之前 Java 类中定义的所有方法:

    data class Product(var name: String, var price: Double) 

前面的类包含了之前 Java 类中定义的所有方法,但没有大量的样板代码需要维护。

在第二章,奠定基础中,我们提到,在 Kotlin 中,使用结构相等运算符(==)将始终在幕后调用equals方法,这意味着我们可以轻松而安全地比较我们的Product数据类的实例:

    data class Product(var name:String, var price:Double) 

    val productA = Product("Spoon", 30.2) 
    val productB = Product("Spoon", 30.2) 
    val productC = Product("Fork", 17.4) 

    print(productA == productA) // prints: true 
    print(productA == productB) // prints: true 
    print(productB == productA) // prints: true 
    print(productA == productC) // prints: false 
    print(productB == productC) // prints: false 

默认情况下,hashCodeequals方法是基于主构造函数中声明的每个属性生成的。在大多数情况下,这已经足够了,但如果我们需要更多的控制,我们仍然可以在数据类中自己重写这些方法。在这种情况下,编译器不会生成默认实现。

toString 方法

生成的方法包含主构造函数中声明的所有属性的名称和值:

    data class Product(var name:String, var price:Double) 
    val productA = Product("Spoon", 30.2) 
    println(productA) // prints: Product(name=Spoon, price=30.2) 

我们实际上可以将有意义的数据记录到控制台或日志文件中,而不是像 Java 中那样记录类名和内存地址(Person@a4d2e77)。这使得调试过程变得简单得多,因为我们有一个适当的、人类可读的格式。

复制方法

默认情况下,Kotlin 编译器还会生成一个适当的copy方法,允许我们轻松创建对象的副本:

    data class Product(var name: String, var price: Double) 

    val productA = Product("Spoon", 30.2) 
    print(productA) // prints: Product(name=Spoon, price=30.2) 

    val productB = productA.copy() 
    print(productB) // prints: Product(name=Spoon, price=30.2) 

Java 没有命名参数语法,因此在调用copy方法的 Java 代码时,我们需要传递所有参数(参数的顺序对应于主构造函数中定义的属性的顺序)。在 Kotlin 中,这种方法减少了对copy构造函数或copy工厂的需求:

  • copy构造函数接受一个参数,类型是包含构造函数的类,并返回这个类的newInstance
        val productB = Product(productA) 
  • copy工厂是一个静态工厂,它接受一个参数,其类型是包含工厂的类,并返回这个类的新实例:
        val productB = ProductFactory.newInstance(productA)

copy方法接受与主构造函数中声明的所有属性相对应的参数。与默认参数语法结合使用时,我们可以提供所有或只有一些属性来创建修改后的实例副本:

    data class Product(var name:String, var price:Double) 

    val productA = Product("Spoon", 30.2) 
    print(productA) // prints: Product(name=Spoon, price=30.2) 

    val productB = productA.copy(price = 24.0) 
    print(productB) // prints: Product(name=Spoon, price=24.0) 

    val productC = productA.copy(price = 24.0, name = "Knife") 
    print(productB) // prints: Product(name=Knife, price=24.0) 

这是创建对象副本的一种非常灵活的方式,我们可以很容易地说出副本应该如何与原始实例不同。另一方面,编程方法提倡不可变性的概念,可以通过无参数调用copy方法轻松实现:

    //Mutable object - modify object state 
    data class Product(var name:String, var price:Double) 

    var productA = Product("Spoon", 30.2) 
    productA.name = "Knife" 

    //immutable object - create new object instance 
    data class Product(val name:String, val price:Double) 

    var productA = Product("Spoon", 30.2) 
    productA = productA.copy(name = "Knife") 

我们可以定义不可变属性(val)而不是定义可变属性(var)并修改对象状态,使对象不可变,并通过获取具有更改值的副本来对其进行操作。这种方法减少了多线程应用程序中数据同步的需求,以及与之相关的潜在错误的数量,因为不可变对象可以在线程之间自由共享。

破坏性声明

有时将对象重构为多个变量是有意义的。这种语法称为解构声明

    data class Person(val firstName: String, val lastName: String, 

                      val height: Int) 

    val person = Person("Igor", "Wojda", 180) 
    var (firstName, lastName, height) = person 
    println(firstName) // prints: "Igor" 
    println(lastName) // prints: "Wojda" 
    println(height) // prints: 180 

解构声明允许我们一次创建多个变量。前面的代码将导致创建firstNamelastNameheight变量的值。在幕后,编译器将生成以下代码:

    val person = Person("Igor", "Wojda", 180) 
    var firstName = person.component1() 
    var lastName = person.component2() 
    var height = person.component3() 

对于数据类的主构造函数中声明的每个属性,Kotlin 编译器将生成一个componentN方法。组件函数的后缀对应于主构造函数中声明的属性的顺序,因此firstName对应于component1lastName对应于component2height对应于component3。实际上,我们可以直接在Person类上调用这些方法来检索属性值,但这样做没有意义,因为它们的名称是无意义的,代码会非常难以阅读和维护。我们应该将这些方法留给编译器来解构对象,并使用属性访问语法,如person.firstName

我们还可以使用下划线省略一个或多个属性:

    val person = Person("Igor", "Wojda", 180) 
    var (firstName, _, height) = person 
    println(firstName) // prints: "Igor" 
    println(height) // prints: 180 

在这种情况下,我们只想创建两个变量,firstNameheightlastName被忽略。编译器生成的代码如下所示:

    val person = Person("Igor", "Wojda", 180) 
    var firstName= person.component1() 
    var height = person.component3() 

我们还可以解构简单类型,如String

    val file = "MainActivity.kt" 
    val (name, extension) = file.split(".", limit = 2) 

破坏性声明也可以与for循环一起使用:

    val authors = listOf( 
       Person("Igor", "Wojda", 180), 
       Person("Marcin", "Moskała", 180) 
    ) 

    println("Authors:") 
    for ((name, surname) in authors) { 
        println("$name $surname") 
    }

运算符重载

Kotlin 具有一组预定义的具有固定符号表示(+,*等)和固定优先级的运算符。大多数运算符直接转换为方法调用;有些转换为更复杂的表达式。以下表格包含 Kotlin 中所有可用运算符的列表:

运算符标记 对应的方法/表达式
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..b a.rangeTo(b)
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)
a++ a.inc()
a-- a.dec()
a in b b.contains(a)
a !in b !b.contains(a)
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)
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

Kotlin 编译器将表示特定操作的标记(左列)转换为将被调用的相应方法或表达式(右列)。

我们可以通过在类operator方法中使用它们来为每个运算符提供自定义实现。让我们定义一个简单的Point类,其中包含xy属性以及两个运算符,plustimes

data class Point(var x: Double, var y: Double) { 
   operator fun plus(point: Point) = Point(x + point.x, y+ point.y) 

   operator fun times(other: Int) = Point(x * other, y * other) 
} 

//usage 
var p1 = Point(2.9, 5.0) 
var p2 = Point(2.0, 7.5) 

println(p1 + p2)     // prints: Point(x=4.9, y=12.5) 
println(p1 * 3)      // prints: Point(x=8.7, y=21.0) 

通过定义plustimes运算符,我们可以对任何Point实例执行加法和乘法操作。每次调用+*操作时,Kotlin 都会调用相应的运算符方法plustimes。在幕后,编译器将生成方法调用:

    p1.plus(p2) 
    p1.times(3) 

在我们的示例中,我们将另一个point实例传递给plus运算符方法,但这种类型并不是强制的。运算符方法实际上并没有覆盖超类中的任何方法,因此它没有固定参数和固定类型的固定声明。我们不必从特定的 Kotlin 类型继承才能重载运算符。我们需要的只是一个具有适当签名的方法,标记为operator。Kotlin 编译器将通过运行与运算符对应的方法来完成其余工作。实际上,我们可以定义多个具有相同名称但不同参数类型的运算符:

data class Point(var x: Double, var y: Double) { 
   operator fun plus(point: Point) = Point(x + point.x, y +point.y) 

   operator fun plus(vector:Double) = Point(x + vector, y + vector) 
} 

var p1 = Point(2.9, 5.0) 
var p2 = Point(2.0, 7.5) 

println(p1 + p2) // prints: Point(x=4.9, y=12.5) 
println(p1 + 3.1) // prints: Point(x=6.0, y=10.1) 

两个运算符都工作正常,因为 Kotlin 编译器可以选择正确的运算符重载。许多基本运算符都有相应的复合赋值运算符(plusplusAssigntimestimesAssign等),因此当我们定义诸如+运算符时,Kotlin 也支持+操作和+=操作:

    var p1 = Point(2.9, 7.0) 
    var p2 = Point(2.0, 7.5) 

    p1 += p2 
    println(p1) // prints: Point(x=4.9, y=14.5) 

注意重要的区别,在某些情况下可能是性能关键。复合赋值运算符(例如,+=运算符)具有Unit返回类型,因此它只是修改现有对象的状态,而基本运算符(例如,+运算符)总是返回对象的新实例:

    var p1 = Wallet(39.0, 14.5) 
    p1 += p2          // update state of p1  
    val p3 = p1 + p2  //creates new object p3      

当我们定义具有相同参数类型的plusplusAssign运算符时,当我们尝试使用plusAssign(复合)运算符时,编译器会抛出错误,因为它不知道应该调用哪个方法:

    data class Point(var x: Double, var y: Double) { 
        init { 
            println("Point created $x.$y") 
        } 
        operator fun plus(point: Point) = Point(x + point.x, y + point.y) 

        operator fun plusAssign(point:Point) { 
            x += point.x 
            y += point.y 
        } 
    } 

    \\usage 
    var p1 = Point(2.9, 7.0) 
    var p2 = Point(2.0, 7.5) 
    val p3 = p1 + p2 
    p1 += p2 // Error: Assignment operations ambiguity 

运算符重载也适用于在 Java 中定义的类。我们所需要的是一个具有与运算符方法名称对应的适当签名和名称的方法。Kotlin 编译器将运算符的使用转换为此方法。Java 中不存在运算符修饰符,因此在 Java 类中不需要它:

    // Java 
    public class Point { 

        private final int x; 
        private final int y; 

        public Point(int x, int y) { 
            this.x = x; 
            this.y = y; 
        } 

        public int getX() { 
            return x; 
        } 

        public int getY() { 
            return y; 
        } 

        public Point plus(Point point) { 
            return new Point(point.getX() + x, point.getY() + y); 
        } 
    } 

    //Main.kt 
    val p1 = Point(1, 2) 
    val p2 = Point(3, 4) 
    val p3 = p1 + p2; 
    println("$x:{p3.x}, y:${p3.y}") //prints: x:4, y:6  

对象声明

在 Java 中有几种声明单例的方法。以下是定义具有私有构造函数并通过静态工厂方法检索实例的类的最常见方法:

    public class Singleton { 

        private Singleton() { 
        } 

        private static Singleton instance; 

        public static Singleton getInstance() { 
            if (instance == null) { 
                instance = new Singleton(); 
            } 

            return instance; 
        } 
    } 

前面的代码对于单个线程来说运行良好,但它不是线程安全的,因此在某些情况下可能会创建两个Singleton实例。有几种方法可以解决这个问题。我们可以使用如下所示的synchronized块:

    //synchronized  
    public class Singleton { 

        private static Singleton instance = null; 

        private Singleton(){ 
        } 

        private synchronized static void createInstance() { 
            if (instance == null) { 
                instance = new Singleton(); 
            } 
        } 

        public static Singleton getInstance() { 
            if (instance == null) createInstance(); 
            return instance; 
        } 
    } 

然而,这种解决方案非常冗长。在 Kotlin 中,有一种特殊的语言构造用于创建称为对象声明的单例,因此我们可以以更简单的方式实现相同的结果。定义对象类似于定义类;唯一的区别是我们使用object关键字而不是class关键字:

    object Singleton 

我们可以像在类中一样向对象声明添加方法和属性:

    object SQLiteSingleton { 
        fun getAllUsers(): List<User> { 
            //... 
        } 
    } 

这种方法的访问方式与任何 Java 静态方法相同:

    SQLiteSingleton.getAllUsers() 

对象声明是延迟初始化的,它们可以嵌套在其他对象声明或非内部类中。此外,它们不能分配给变量。

对象表达式

对象表达式等效于 Java 的匿名类。它用于实例化可能继承自某个类或实现接口的对象。一个经典的用例是当我们需要定义实现某个接口的对象时。这就是在 Java 中我们如何实现ServiceConnection接口并将其分配给一个变量的方式:

    ServiceConnection serviceConnection = new ServiceConnection() { 
        @Override 
        public void onServiceDisconnected(ComponentName name) { 
            ...

        } 

        @Override 
        public void onServiceConnected(ComponentName name, 

            IBinder service) 

        {

            ... 
        } 
    } 

前面实现的最接近 Kotlin 等效的是以下内容:

    val serviceConnection = object: ServiceConnection { 

     override fun onServiceDisconnected(name: ComponentName?) { } 

     override fun onServiceConnected(name: ComponentName?, 

         service: IBinder?) { } 
    } 

前面的示例使用了对象表达式,它创建了一个实现ServiceConnection接口的匿名类的实例。对象表达式也可以扩展类。以下是我们如何创建抽象类BroadcastReceiver的实例的方式:

    val broadcastReceiver = object : BroadcastReceiver() { 
       override fun onReceive(context: Context, intent: Intent) { 
           println("Got a broadcast ${intent.action}") 
       } 
    } 

    val intentFilter = IntentFilter("SomeAction"); 
    registerReceiver(broadcastReceiver, intentFilter) 

虽然对象表达式允许我们创建一个匿名类型的对象,该对象可以实现某个接口并扩展某个类,但我们可以使用它们轻松解决与适配器模式相关的有趣问题。

适配器设计模式允许通过将一个类的接口转换为客户端期望的接口,使本来不兼容的类一起工作。

假设我们有一个Player接口和一个需要Player作为参数的函数:

    interface Player { 
        fun play() 
    } 

    fun playWith(player: Player) { 
        print("I play with") 
        player.play() 
    } 

此外,我们从一个公共库中有一个VideoPlayer类,该类定义了play方法,但它没有实现我们的Player接口:

    open class VideoPlayer { 
        fun play() { 
            println("Play video") 
        } 
    } 

VideoPlayer类满足所有接口要求,但不能作为Player传递,因为它没有实现该接口。要将其用作播放器,我们需要创建一个适配器。在这个例子中,我们将其实现为一个匿名类型的对象,该对象实现了Player接口:

    val player = object: VideoPlayer(), Player { } 
    playWith(player) 

我们能够解决问题而不定义VideoPlayer子类。我们还可以在对象表达式中定义自定义方法和属性:

    val data = object { 
        var size = 1 
        fun update() { 
            //... 
        } 
    } 

    data.size = 2 
    data .update() 

这是一种非常简单的方法来定义在 Java 中不存在的自定义匿名对象。要在 Java 中定义类似的类型,我们需要定义自定义接口。现在我们可以为VideoPlayer类添加行为,以完全实现Player接口:

    open class VideoPlayer { 
        fun play() { 
            println("Play video") 
        } 
    } 

    interface Player{ 
        fun play() 
        fun stop() 
    } 

    //usage 
    val player = object: VideoPlayer(), Player { 
        var duration:Double = 0.0 

        fun stop() { 
            println("Stop  video") 
        } 
    } 

    player.play() // println("Play video") 
    player.stop() // println("Stop  video") 
    player.duration = 12.5 

在上述代码中,我们可以调用在VideoPlayer类中定义的匿名对象(player)方法和表达对象。

伴生对象

与 Java 相反,Kotlin 缺乏定义静态成员的能力,但它允许我们定义与类相关联的对象。换句话说,对象只初始化一次;因此只存在一个对象实例,跨特定类的所有实例共享其状态。当一个单例对象与同名类相关联时,它被称为该类的伴生对象,而该类被称为该对象的伴生类

上图展示了Car类的三个实例共享一个对象实例。

在伴生对象内定义的成员,如方法和属性,可以类似于我们在 Java 中访问静态字段和方法的方式进行访问。伴生对象的主要目的是拥有与类相关但不一定与该类的任何特定实例相关的代码。这是定义在 Java 中将被定义为静态的成员的一个很好的方式;例如,工厂,它创建一个类实例方法转换一些单位,活动请求代码,共享首选项键等。要定义最简单的伴生对象,我们需要定义一个代码块:

    class ProductDetailsActivity { 

        companion object { 
        } 
    } 

现在让我们定义一个start方法,这将允许我们以一种简单的方式启动一个活动:

    //ProductDetailsActivity.kt 
    class ProductDetailsActivity : AppCompatActivity() { 

        override fun onCreate(savedInstanceState: Bundle?) { 
            super.onCreate(savedInstanceState) 
            val product = intent.getParcelableExtra<Product>

                (KEY_PRODUCT) // 3 
            //... 
        } 

        companion object { 

            const val KEY_PRODUCT = "product" // 1 

            fun start(context: Context, product: Product) { // 2 
                val intent = Intent(context, 

                    ProductDetailsActivity::class.java) 
                intent.putExtra(KEY_PRODUCT, product) // 3 
                context.startActivity(intent) 
            } 
        }   
    } 

    // Start activity 
    ViewProductActivity.start(context, productId) // 2 
  1. 只存在一个key的单个实例

  2. 方法start可以在不创建对象实例的情况下调用。就像 Java 静态方法一样。

  3. 在实例创建后检索值。

请注意,我们能够在活动实例创建之前调用start。让我们使用伴生对象来跟踪Car类的实例数量。为了实现这一点,我们需要定义具有私有 setter 的count属性。它也可以定义为顶级属性,但最好将其放在伴生对象内部,因为我们不希望在类外部允许计数器修改:

    class Car { 
        init { 
            count++; 
        } 

        companion object { 
            var count:Int = 0 
            private set 
        } 
    } 

类可以访问伴生对象中定义的所有方法和属性,但伴生对象无法访问任何类内容。伴生对象分配给特定类,但不分配给特定实例:

    println(Car.count) // Prints 0   
    Car() 
    Car() 
    println(Car.count) // Prints: 2 

要直接访问伴生对象的实例,我们可以使用类名。

我们还可以通过更冗长的语法Car.Companion.count访问伴生对象,但在大多数情况下没有必要这样做,除非我们想从 Java 代码中访问companion

伴生对象实例化

伴生对象是由伴生类创建并保存在其静态属性中的单例。companion对象的实例化是懒惰的。这意味着companion对象将在首次需要时实例化--当访问其成员或创建包含companion对象的类的实例时。要标记Car类实例及其对应的companion对象何时创建,我们需要添加两个初始化块--一个用于Car类,另一个用于伴生对象。

companion对象内的初始化块与类中的初始化块完全相同--它在实例创建时执行:

    class Car { 
        init { 
            count++; 
            println("Car created") 
        } 

        companion object { 
            var count: Int = 0 

            init { 
                println("Car companion object created") 
            } 
        }  
    } 

虽然类初始化块相当于 Java 构造函数体,但编译对象初始化块相当于 Kotlin 中的 Java 静态初始化块。目前,count属性可以被任何客户端更新,因为它可以从Car类的外部访问。我们将在本章的Visibility修饰符部分解决这个问题。现在让我们访问Carcompanion对象类成员:

    Car.count  // Prints: Car companion object created 
    Car() // Prints: Car created 

通过访问companion对象中定义的count属性,我们触发了它的创建,但请注意,Car类的实例并没有被创建。稍后当我们创建Car类的实例时,companion对象已经被创建。现在让我们在访问companion对象之前实例化Car类:

    Car()  
    //Prints: Car companion object created 
    //Prints: Car created 

    Car()  //Prints: Car created 
    Car.count 

companion对象与Car类的第一个实例一起创建,因此当我们创建用户类的其他实例时,该类的companion对象已经存在,因此不会被创建。

请记住,前面的实例描述了两个不同的示例。两者不能同时在一个程序中成立,因为类的companion对象只能存在一个实例,并且它是在需要时第一次创建的。

companion对象也可以包含函数、实现接口,甚至扩展类。我们可以定义一个companion对象,其中包括一个静态构造方法,还可以覆盖实现以供测试目的使用:

  abstract class Provider<T> { // 1 

     abstract fun creator(): T // 2 

     private var instance: T? = null // 3 
     var override: T? = null // 4 

     fun get(): T = override ?: instance ?: creator().also { instance = it } //5 
  } 
  1. Provider 是一个泛型类。

  2. 用于创建实例的抽象函数。

  3. 用于保存已创建实例的字段。

  4. 用于测试的字段,提供实例的替代实现。

  5. 返回覆盖实例(如果已设置),创建实例(如果已创建),或者使用 create 方法创建实例并将实例字段填充的函数。

通过这样的实现,我们可以定义一个带有默认静态构造函数的接口:

  interface MarvelRepository { 

     fun getAllCharacters(searchQuery: String?): Single<List<MarvelCharacter>> 

     companion object : Provider<MarvelRepository>() { 
         override fun creator() = MarvelRepositoryImpl() 
     } 
  } 

要获取实例,我们需要使用:

    MarvelRepository.get() 

如果我们需要为测试目的指定其他实例(例如,在 Espresso 测试中),那么我们总是可以使用对象表达式来指定它们:

    MarvelRepository.override = object : MarvelRepository { 
        override fun getAllCharacters(searchQuery: String?): 

        Single<List<MarvelCharacter>> { 
            //... 
        } 
    } 

companion对象在 Kotlin Android 世界中非常受欢迎。它们主要用于定义在 Java 中是静态的所有元素(常量字段、静态创建者等),但它们还提供了额外的功能。

枚举类

枚举类型(enum)是由一组命名值组成的数据类型。要定义枚举类型,我们需要在类声明头部添加enum关键字:

    enum class Color { 
        RED, 
        ORANGE, 
        BLUE, 
        GRAY, 
        VIOLET 
    } 

    val favouriteColor = Color.BLUE 

将字符串解析为enum,使用valueOf方法(就像在 Java 中一样):

    val selectedColor = Color.valueOf("BLUE") 
    println(selectedColor == Color.BLUE) // prints: true 

或者使用 Kotlin 辅助方法:

    val selectedColor = enumValueOf<Color>("BLUE") 
    println(selectedColor == Color.BLUE) // prints: true 

要显示Color枚举中的所有值,使用 values 函数(就像在 Java 中一样):

    for (color in Color.values()) { 
        println("name: ${it.name}, ordinal: ${it.ordinal}") 
    } 

或者使用 Kotlin 的enumerateValues辅助方法:

    for (color in enumValues<Color>()) { 
        println("name: ${it.name}, ordinal: ${it.ordinal}")    
    } 

    // Prints: 
    name: RED, ordinal: 0 
    name: ORANGE, ordinal: 1 
    name: BLUE, ordinal: 2 
    name: GRAY, ordinal: 3 
    name: VIOLET, ordinal: 4 

enum类型也可以有自己的构造函数,并且可以为每个enum常量关联自定义数据。让我们添加具有redgreenblue颜色分量值的属性:

    enum class Color(val r: Int, val g: Int, val b: Int) { 
        RED(255, 0, 0), 
        ORANGE(255, 165, 0), 
        BLUE(0, 0, 255), 
        GRAY(49, 79, 79), 
        VIOLET(238, 130, 238) 
    } 

    val color = Color.BLUE 
    val rValue =color.r 
    val gValue = color.g 
    val bValue = color.b 

有了这些值,我们可以定义一个函数,用于计算每种颜色的 RGB 值。

请注意,最后一个常量(VIOLET)后面跟着一个分号。这是 Kotlin 代码中实际需要分号的罕见情况。它将常量定义与成员定义分开:

    enum class Color(val r: Int, val g: Int, val b: Int) { 
        BLUE(0, 0, 255), 
        ORANGE(255, 165, 0), 
        GRAY(49, 79, 79), 
        RED(255, 0, 0), 
        VIOLET(238, 130, 238); 

        fun rgb() = r shl 16 + g shl 8 + b 
    } 

    fun printHex(num: Int) { 
        println(num.toString(16)) 
    } 

    printHex(Color.BLUE.rgb()) // Prints: ff 
    printHex(Color.ORANGE.rgb()) // Prints: ffa500 
    printHex(Color.GRAY.rgb()) // Prints: 314f4f 

rgb()方法访问特定枚举的rgb变量数据,并分别计算每个enum元素的值。我们还可以使用init块和 Kotlin 标准库的require函数为枚举构造函数参数添加验证:

    enum class Color(val r: Int, val g: Int, val b: Int) { 
        BLUE(0, 0, 255), 
        ORANGE(255, 165, 0), 
        GRAY(49, 79, 79), 
        RED(255, 0, 0), 
        VIOLET(238, 130, 238); 

        init { 
            require(r in 0..255) 
            require(g in 0..255) 
            require(b in 0..255) 
       } 

       fun rgb() = r shl 16 + g shl 8 + b 
    } 

定义不正确的枚举将导致异常:

    GRAY(33, 33, 333) // IllegalArgumentException: Failed requirement. 

有些情况下,我们希望将与每个常量基本不同的行为关联起来。为此,我们可以在每个枚举块中定义一个抽象方法或属性,并在其中重写它。让我们定义枚举Temperaturetemperature属性:

    enum class Temperature { COLD, NEUTRAL, WARM } 

    enum class Color(val r: Int, val g: Int, val b: Int) { 
        RED(255, 0, 0) { 
            override val temperature = Temperature.WARM 
        },  
        ORANGE(255, 165, 0) { 
            override val temperature = Temperature.WARM 
        },  
        BLUE(0, 0, 255) { 
            override val temperature = Temperature.COLD 
        }, 
        GRAY(49, 79, 79) { 
            override val temperature = Temperature.NEUTRAL 
        },  
        VIOLET(238, 130, 238 { 
            override val temperature = Temperature.COLD 
        }; 

        init { 
            require(r in 0..256) 
            require(g in 0..256) 
            require(b in 0..256) 
        } 

        fun rgb() = (r * 256 + g) * 256 + b 

        abstract val temperature: Temperature 
    } 

    println(Color.BLUE.temperature) //prints: COLD 
    println(Color.ORANGE.temperature) //prints: WARM 
    println(Color.GRAY.temperature) //prints: NEUTRAL 

现在,每种颜色不仅包含 RGB 信息,还包含描述其温度的额外枚举。我们已经添加了一个属性,但以类似的方式,我们可以为每个枚举元素添加自定义方法。

命名方法的中缀调用

中缀调用是 Kotlin 的一个特性,它允许我们创建更流畅和可读的代码。它允许我们编写更接近自然人类语言的代码。我们已经在第二章中看到了中缀方法的用法,它允许我们轻松地创建Pair类的实例。这里是一个快速提醒:

    var pair = "Everest" to 8848 

Pair类表示两个值的通用对。在这个类中,值没有附加的含义,因此它可以用于任何目的。Pair是一个数据类,因此它包含所有数据类方法(equalshashCodecomponent1等)。以下是来自 Kotlin 标准库的Pair类的定义:

    public data class Pair<out A, out B>( // 1 
       public val first: A, 
       public val second: B 
    ) : Serializable { 

       public override fun toString(): String = "($first, $second)" 

       // 2 
    } 
  1. 在泛型类型后面使用的out修饰符的含义将在第六章中描述,泛型是你的朋友。

  2. 对有自定义toString方法的对。这是为了使打印语法更可读,而第一个和第二个名称在大多数使用情况下都没有意义。

在我们深入学习如何定义我们自己的中缀方法之前,让我们将所提供的代码翻译成更熟悉的形式。每个中缀方法都可以像任何其他方法一样使用:

    val mountain = "Everest"; 
    var pair = mountain.to(8848) 

本质上,中缀表示法只是调用方法而不使用点运算符和调用运算符(括号)的能力。中缀表示法看起来不同,但在底层仍然是常规方法调用。在上述两个例子中,我们只是在String类实例上调用to方法。to是一个扩展函数,将在第七章中解释,扩展函数和属性,但我们可以想象它是String类的方法,在这种情况下,它只是返回一个包含自身和传递的参数的Pair实例。我们可以像对待任何数据类对象一样操作返回的Pair

    val mountain = "Everest"; 
    var pair = mountain.to(8848) 
    println(pair.first) //prints: Everest 
    println(pair.second) //prints: 8848 

在 Kotlin 中,当方法只有一个参数时,才允许使用中缀方法。此外,中缀表示法不会自动发生——我们需要显式地将方法标记为中缀。让我们用中缀方法定义我们的Point类:

    data class Point(val x: Int, val y: Int) {

        infix fun moveRight(shift: Int) = Point(x + shift, y)

    }

用法示例:

    val pointA = Point(1,4) 
    val pointB = pointA moveRight 2 
    println(pointB) //prints: Point(x=3, y=4) 

请注意,我们正在创建一个新的Point实例,但我们也可以修改现有的实例(如果类型是可变的)。这个决定是开发人员做出的,但中缀更常用于不可变类型。

我们可以将infix方法与枚举结合使用,实现非常流畅的语法。让我们实现自然语法,以便从经典扑克牌中定义卡片。它包括 52 张牌:每种四种花色的 13 张牌:梅花、方块、红桃和黑桃。

上述图像的来源:mathematica.stackexchange.com/questions/16108/standard-deck-of-52-playing-cards-in-curated-data

目标是定义语法,使我们能够以这种方式定义卡片的花色和等级:

    val card = KING of HEARTS 

首先,我们需要两个枚举来表示所有的等级和花色:

    enum class Suit { 
        HEARTS,  
        SPADES,  
        CLUBS,  
        DIAMONDS 
    } 

    enum class Rank { 
        TWO, THREE, FOUR, FIVE, 
        SIX, SEVEN, EIGHT, NINE, 
        TEN, JACK, QUEEN, KING, ACE;  
    } 

然后我们需要一个类来表示由特定等级和特定套房组成的卡片:

    data class Card(val rank: Rank, val suit: Suit) 

现在我们可以像这样实例化一个Card类:

    val card = Card(Rank.KING, Suit.HEARTS) 

为了简化语法,我们在Rank枚举中引入了一个新的中缀方法:

    enum class Rank { 
        TWO, THREE, FOUR, FIVE, 
        SIX, SEVEN, EIGHT, NINE, 
        TEN, JACK, QUEEN, KING, ACE; 

        infix fun of(suit: Suit) = Card(this, suit) 
    } 

这将允许我们创建一个像这样的Card调用:

    val card = Rank.KING.of(Suit.HEARTS) 

因为该方法被标记为中缀,所以我们可以删除点调用运算符和括号:

    val card = Rank.KING of Suit.HEARTS 

使用静态导入将允许我们缩短语法,甚至实现我们的最终结果:

    import Rank.KING 
    import Suit.HEARTS 

    val card = KING of HEARTS 

除了非常简单之外,这段代码还是 100%类型安全的。我们只能使用预定义的RankSuit枚举来定义卡片,因此我们无法错误地定义一些虚构的卡片。

可见性修饰符

Kotlin 支持四种类型的可见性修饰符(访问修饰符)--privateprotectedpublicinternal。Kotlin 不支持包私有 Java 修饰符。主要区别在于,Kotlin 中的默认可见性修饰符是public,不需要显式指定,因此可以省略特定声明。所有修饰符都可以应用于基于其声明位置分为两个主要组的各种元素:顶层元素和嵌套成员。

来自第三章的快速提醒,玩转函数,顶层元素是直接在 Kotlin 文件内声明的元素,而不是嵌套在类、对象、接口或函数内的元素。在 Java 中,我们只能在顶层声明类和接口,而 Kotlin 还允许在那里声明函数、对象、属性和扩展。

首先,我们有顶层元素的可见性修饰符:

  • public(默认):元素在任何地方可见。

  • private:元素在包含声明的文件内可见。

  • protected:在顶层不可用。

  • internal:元素在同一模块中随处可见。对于同一模块中的元素,它是公共的。

Java 和 Kotlin 中的模块是什么?

模块只是一组一起编译的 Kotlin 文件;例如,IntelliJ IDEA 模块、Gradle 项目。应用程序的模块化结构允许更好地分布责任,并加快构建时间,因为只重新编译了更改的模块。

让我们看一个例子:

    //top.kt  
    public val version: String = "3.5.0" // 1 

    internal class UnitConveter // 3 

    private fun printSomething() {  
        println("Something") 
    } 

    fun main(args: Array<String>) { 
        println(version) // 1, Prints: "3.5.0" 
        UnitConveter() // 2, Accessible 
        printSomething() // 3, Prints: Something 
    } 

    // branch.kt 
    fun main(args: Array<String>) { 
        println(version) // 1, Accessible
        UnitConveter() // 2, Accessible
        printSomething() // 3, Error 
    } 

    // main.kt in another module 
    fun main(args: Array<String>) { 
        println(version) // 1, Accessible 
        UnitConveter() // 2, Error 
        printSomething() // 3, Accessible
    } 
  1. version属性是公共的,因此可以在所有文件中访问。

  2. UnitConveter在 branch.kt 文件中可访问,因为它在同一模块中,但在main.kt中不可访问,因为它位于另一个模块中。

  3. printSomething函数只能在定义它的同一文件中访问。

请注意,Kotlin 中的包不会提供任何额外的可见性特权。

第二组成员包括在顶层元素内声明的元素。主要是方法、属性、构造函数,有时是对象、伴生对象、getter 和 setter,偶尔是嵌套类和嵌套接口。以下是必须遵守的规则:

  • public(默认):看到声明类的客户端可以看到其公共成员。

  • private:元素仅在包含成员的类或接口内部可见。

  • protected:在包含声明的类和子类内可见。它不适用于对象内部,因为对象无法被打开。

  • internal:在此模块内看到声明类的任何客户端都可以看到其内部成员。

让我们定义一个顶层元素。在这个例子中,我们将定义类,但相同的逻辑适用于任何具有嵌套成员的顶层元素:

    class Person { 
        public val name: String = "Igor" 
        protected var age:Int = 23 
        internal fun learn() {} 
        private fun speak() {} 
    } 

当我们创建Person类的实例时,我们只能访问用 public 修饰符标记的name属性和用 internal 修饰符标记的learn方法:

    // main.kt inside the same package as Person definition 
    val person = Person() 
    println(person.name)   // 1 
    person.speak()         // 2, Error 
    person.age             // 3, Error 
    person.learn()         // 4 
  1. 可以访问Person实例的client也可以访问name属性。

  2. speak方法只能在Person类内部访问。

  3. age属性在Person类及其子类内部可访问。

  4. 在可以访问Person类实例的模块内的client也可以访问其public成员。

继承可访问性类似于外部访问可访问性,但主要区别在于,标记为protected修饰符的成员也在子类内可见:

    open class Person { 
        public val name: String = "Igor" 
        private fun speak() {} 
        protected var age: Int = 23 
        internal fun learn() {} 
    } 

    class Student() : Person() { 
        fun doSth() { 
            println(name) 
            learn() 
            print(age) 
            // speak()  // 1 
        } 
    }  
  1. Student子类中,我们可以访问标记为 public、protected 和 internal 的成员,但不能访问标记为 private 修饰符的成员。

内部修饰符和 Java 字节码

很明显,publicprivateprotected修饰符在编译为 Java 时是如何直接对应的。但是,内部修饰符存在问题,因为它在 Java 中没有直接对应,因此在 Java 字节码上也没有支持。这就是为什么内部修饰符实际上被编译为public修饰符,并且为了表明它不应该在 Java 中使用,它的名称被改变(改变以使其不再可用)。例如,当我们有Foo类时:

    open class Foo { 
        internal fun boo() { } 
    } 

可以通过以下方式从 Java 中使用它:

    public class Java { 
        void a() { 
            new Foo().boo$production_sources_for_module_SmallTest(); 
       } 
    } 

内部可见性受到 Kotlin 的保护,可以通过 Java 适配器绕过,这是相当有争议的,但没有其他可能性来实现它。

除了在类中定义可见性修饰符,我们还能够在覆盖成员时覆盖它们。这使我们能够在继承层次结构中减弱访问限制:

    open class Person { 
        protected open fun speak() {} 
    } 

    class Student() : Person() { 
        public override fun speak() { 
        } 
    } 

    val person = Person() 
    //person.speak() // 1 

    val student = Student() 
    student.speak() // 2
  1. 错误,speak 方法不可访问,因为它是受保护的。

  2. speak 方法的可见性已更改为 public,以便我们可以访问它。

定义成员和它们的可见性范围的修饰符非常简单,所以让我们看看如何定义类和构造函数的可见性。正如我们所知,主构造函数定义在类头中,因此在一行中需要两个可见性修饰符:

    internal class Fruit private constructor { 
       var weight: Double? = null 

       companion object { 
           fun create() = Fruit() 
       } 
    } 

假设前面的类是在顶层定义的,它将在模块内可见,但只能在包含类声明的文件内实例化:

    var fruit: Fruit? = null    // Accessible
    fruit = Fruit()             // Error 
    fruit = Fruit.create()      // Accessible

getter 和 setter 默认具有与属性相同的可见性修饰符,但我们可以修改它。Kotlin 允许我们在get/set关键字之前放置可见性修饰符:

    class Car { 
        init { 
            count++; 
            println("Car created") 
        } 

        companion object { 
            init { 
                println("Car companion object created") 
            } 

            var count: Int = 0 
                private set 
        } 
    } 

在前面的示例中,我们已更改了 getter 的可见性。请注意,这种方法允许我们更改可见性修饰符,而不更改其默认实现(由编译器生成)。现在,我们的实例计数器是安全的,因为它是只读的外部客户端,但我们仍然可以从Car类内部修改属性值。

封闭类

封闭类是具有有限子类的类(封闭子类型层次结构)。在 Kotlin 1.1 之前,这些子类必须在封闭类主体内定义。Kotlin 1.1 放宽了这一限制,并允许我们在同一文件中定义封闭类的子类声明。所有类都在彼此附近声明,因此我们可以通过简单地查看一个文件来轻松看到所有可能的子类:

    //vehicle.kt 

    sealed class Vehicle()
    class Car : Vehicle()
    class Truck : Vehicle()
    class Bus : Vehicle()

要将类标记为封闭类,只需在类声明头部添加sealed修饰符。前面的声明意味着Vehicle类只能由三个类CarTruckBus扩展,因为它们在同一个文件中声明。我们可以在vehicle.kt文件中添加第四个类,但不可能在另一个文件中定义这样的类。

sealed子类型限制仅适用于Vehicle类的直接继承者。这意味着Vehicle只能由在同一文件中定义的类(CarTruckBus)扩展,但假设CarTruckBus类是开放的,那么它们可以由在任何文件中声明的类扩展:

    //vehicle.kt 
    sealed class Vehicle() 
    open class Bus : Vehicle() 

    //data.kt 
    class SchoolBus:Bus() 

要防止这种行为,我们还需要将CarTruckBus类标记为 sealed:

    //vehicle.kt 
    sealed class Vehicle() 
    sealed class Bus : Vehicle() 

    //data.kt 
    class SchoolBus:Bus() //Error cannot access Bus 

封闭类与when表达式非常配合。无需else子句,因为编译器可以验证封闭类的每个子类在when块内有相应的子句:

    when (vehicle) { 
        is Car -> println("Can transport 4 people") 
        is Truck -> println("Can transport furnitures ") 
        is Bus -> println("Can transport 50 people ") 
    } 

我们可以安全地将一个新的子类添加到Vehicle类中,因为如果应用程序中的when表达式的相应子句缺失,应用程序将无法编译。这修复了 Java switch语句的问题,程序员经常忘记添加适当的封装,导致运行时程序崩溃或未检测到的错误。

密封类默认是抽象的,因此抽象修饰符是多余的。密封类永远不能是openfinal。我们还可以用对象替换子类,以确保只存在一个实例:

    sealed class Employee() 

    class Programmer : Employee() 
    class Manager : Employee() 
    object CEO : Employee() 

前面的声明不仅保护了继承层次结构,还限制了 CEO 只能有一个实例。密封类有一些有趣的应用超出了本书的范围,但了解它们是很好的:

嵌套类

嵌套类是在另一个类内部定义的类。将小类嵌套在顶级类中可以使代码更接近其使用位置,并允许更好地对类进行分组。典型的例子是Tree/Leaf监听器或演示状态。Kotlin 与 Java 类似,允许我们定义嵌套类,有两种主要方法可以这样做。我们可以将类定义为类的成员:

    class Outer { 
        private val bar: Int = 1 

        class Nested { 
            fun foo() = 2 
        } 
    } 

    val demo = Outer.Nested().foo() // == 2 

前面的例子允许我们创建一个Nested类的实例,而不创建Outer类的实例。在这种情况下,一个类不能直接引用其封闭类中定义的实例变量或方法(它只能通过对象引用来使用它们)。这相当于 Java 的静态嵌套类和一般静态成员。

为了能够访问外部类的成员,我们必须通过将嵌套类标记为inner来创建第二种类:

    class Outer { 
        private val bar: Int = 1 

        inner class Inner { 
            fun foo() = bar 
        } 
    } 

    val outer = Outer() 
    val demo = outer.Inner().foo() // == 1 

现在要实例化inner类,我们必须首先实例化Outer类。在这种情况下,Inner类可以访问外部类中定义的所有方法和属性,并与外部类共享状态。每个Outer类的实例只能存在一个Inner类的实例。让我们总结一下区别:

行为 类(成员) 内部类(成员)
表现为 Java 的静态成员
此类的实例可以存在而不需要封闭类的实例
有对外部类的引用
与外部类共享状态(可以访问外部类成员)
实例数量 无限 每个外部类实例一个

在决定是否应该定义inner类或顶级类时,我们应该考虑潜在的类使用情况。如果该类只对单个类实例有用,我们应该将其声明为inner。如果inner类在某个时刻对除了为其外部类服务之外的其他上下文有用,那么我们应该将其声明为顶级类。

导入别名

别名是引入类型的新名称的一种方式。如果类型名称已在文件中使用,不合适或太长,可以引入不同的名称并在编写代码时使用它而不是原始类型名称。别名不会引入新类型,它只在编译时(编写代码时)可用。编译器将类别名替换为实际类,因此在运行时它不存在。

有时我们需要在单个文件中使用几个同名类。例如,InterstitialAd 类型在 Facebook 和 Google 广告库中都有定义。假设我们想在单个文件中同时使用它们。这种情况在需要实现两个广告提供商以允许它们之间的利润比较的项目中很常见。问题是在单个文件中使用两种数据类型意味着我们需要通过完全限定的类名(命名空间+类名)访问其中一个或两个。

    import com.facebook.ads.InterstitialAd 

    val fbAd = InterstitialAd(context, "...") 
    val googleAd = com.google.android.gms.ads.InterstitialAd(context)

限定与未限定的类名

未限定的类名只是类的名称;例如,Box。限定的类名是命名空间与类名的组合;例如,com.test.Box

在这些情况下,人们经常说最好的解决方法是重命名其中一个类,但有时这可能不可行(类是在外部库中定义的)或不可取(类名与后端数据库表一致)。在这种情况下,当两个类都位于外部库中时,解决类命名冲突的方法是使用import别名。我们可以使用它将 Google 的InterstitialAd重命名为GoogleAd,将 Facebook 的InterstitialAd重命名为FbAd

    import com.facebook.ads.InterstitialAd as FbAd 
    import com.google.android.gms.ads.InterstitialAd as GoogleAd 

现在我们可以在文件中使用这些别名,就好像它们是实际的类型一样:

    val fbAd = FbAd(context, "...") 
    val googleAd = GoogleAd(context) 

使用import别名,我们可以明确地重新定义导入文件中的类的名称。在这种情况下,我们不必使用两个别名,但这有助于提高可读性--拥有FbAdGoogleAd要比InterstitialAdGoogleAd更好。我们不再需要使用完全限定的类名,因为我们只是告诉编译器"每当你遇到GoogleAd别名时,在编译期间将其转换为com.google.android.gms.ads.InterstitialAd,每当你遇到FbAd别名时,将其转换为com.facebook.ads.InterstitialAd。导入别名仅在定义别名的文件内起作用。

总结

在本章中,我们讨论了面向对象编程的构造,这些构造是对象导向编程的基础。我们学会了如何定义接口和各种类,以及innersealedenum和数据类之间的区别。我们了解到所有元素默认都是公共的,所有类/接口默认都是final默认情况下),因此我们需要明确地打开它们以允许继承和成员重写。

我们讨论了如何使用非常简洁的数据类结合更强大的属性来定义适当的数据模型。我们知道如何使用编译器生成的各种方法来正确操作数据,以及如何重载运算符。

我们学会了如何使用对象声明创建单例,以及如何使用对象表达式定义匿名类型的对象,这些对象可以扩展某个类和/或实现某个接口。我们还介绍了lateinit修饰符的用法,它允许我们延迟初始化非空数据类型。

在下一章中,我们将通过研究与函数式编程FP)相关的概念来讨论 Kotlin 更加功能性的一面。我们将讨论函数类型、lambda 和高阶函数。

第五章:函数作为一等公民

在上一章中,我们看到了 Kotlin 特性与面向对象编程的关系。本章将介绍以前在标准 Android 开发中不存在的高级函数式编程特性。其中一些在 Java 8 中引入(通过 Retrolambda 插件在 Android 中引入),但 Kotlin 引入了更多的函数式编程特性。

这一章是关于高级函数和函数作为一等公民的。大多数概念对于过去使用过函数式语言的读者来说都是熟悉的。

在本章中,我们将涵盖以下主题:

  • 函数类型

  • 匿名函数

  • Lambda 表达式

  • lambda 表达式中单个参数的隐式名称

  • 高阶函数

  • 最后一个参数中的 lambda 约定

  • Java Single Abstract MethodSAM)lambda 接口

  • 在参数上使用 Java 方法和 Java Single Abstract Method

  • 函数类型中的命名参数

  • 类型别名

  • 内联函数

  • 函数引用

函数类型

Kotlin 支持函数式编程,函数在 Kotlin 中是一等公民。在给定的编程语言中,一等公民是指支持其他实体通常可用的所有操作的实体。这些操作通常包括作为参数传递,从函数返回,并分配给变量。因此,“函数在 Kotlin 中是一等公民”这句话应该被理解为:“在 Kotlin 中可以将函数作为参数传递,从函数返回,并将其分配给变量”。虽然 Kotlin 是一种静态类型语言,但需要定义函数类型以允许这些操作。在 Kotlin 中,用于定义函数类型的表示法如下:

    (types of parameters)->return type 

以下是一些例子:

  • (Int)->Int:接受Int作为参数并返回Int的函数

  • ()->Int:不接受任何参数并返回Int的函数

  • (Int)->Unit:接受Int并不返回任何东西(只有Unit,不需要返回)

以下是一些可以保存函数的属性的示例:

    lateinit var a: (Int) -> Int   
    lateinit var b: ()->Int 
    lateinit var c: (String)->Unit 

函数类型通常被定义为变量或参数的类型,可以将函数分配给它们,或者作为高阶函数的参数或结果类型。在 Kotlin 中,函数类型可以被视为接口。

我们将在本章后面看到,Kotlin 函数可以在参数中接受其他函数,甚至返回它们:

    fun addCache(function: (Int) -> Int): (Int) -> Int { 
        // code 
    } 

    val fibonacciNumber: (Int)->Int = // function implementation 
    val fibonacciNumberWithCache = addCache(fibonacciNumber) 

如果一个函数可以接受或返回一个函数,那么函数类型也需要能够定义接受函数作为参数或返回函数的函数。这可以通过简单地将函数类型表示法放置为参数或返回类型来实现。以下是一些例子:

  • (String)->(Int)->Int:接受String并返回一个接受Int类型并返回Int的函数。

  • (()->Int)->String:接受另一个函数作为参数,并返回String类型的函数。参数中的函数不接受任何参数,并返回Int

每个带有函数类型的属性都可以像函数一样被调用:

    val i = a(10) 
    val j = b() 
    c("Some String") 

函数不仅可以存储在变量中,还可以用作泛型。例如,我们可以将函数保存在列表中:

    var todoList: List<() -> Unit> = // ... 
    for (task in todoList) task() 

前面的列表可以存储带有() -> Unit签名的函数。

底层的函数类型是什么?

在底层,函数类型只是泛型接口的一种语法糖。让我们看一些例子:

  • ()->Unit签名是Function0<Unit>的接口。表达式是Function0,因为它没有参数,而Unit是返回类型。

  • (Int)->Unit签名是Function1<Int, Unit>的接口。表达式是Function1,因为它有一个参数。

  • ()->(Int, Int)->String签名是Function0<Function2<Int, Int, String>>的接口。

所有这些接口只有一个方法,invoke,它是一个操作符。它允许对象像函数一样使用:

    val a: (Int) -> Unit = //... 
    a(10)        // 1 
    a.invoke(10) // 1 
  1. 这两个语句具有相同的含义

函数接口不在标准库中。它们是合成的编译器生成的类型(它们在编译过程中生成)。因此,函数类型参数的数量没有人为限制,标准库的大小也不会增加。

匿名函数

定义函数为对象的一种方式是使用匿名函数。它们的工作方式与普通函数相同,但在fun关键字和参数声明之间没有名称,因此默认情况下它们被视为对象。以下是一些示例:

    val a: (Int) -> Int = fun(i: Int) = i * 2 // 1 
    val b: ()->Int = fun(): Int { return 4 } 
    val c: (String)->Unit = fun(s: String){ println(s) } 
  1. 这是一个匿名的单表达式函数。请注意,与普通的单表达式函数一样,当它从表达式返回类型中推断出时,不需要指定返回类型。

考虑以下用法:

    // Usage 
    println(a(10))      // Prints: 20 
    println(b())        // Prints: 4 
    c("Kotlin rules")   // Prints: Kotlin rules 

在前面的示例中,函数类型是显式定义的,但是由于 Kotlin 有很好的类型推断系统,函数类型也可以从匿名默认函数定义的类型中推断出来:

    var a = fun(i: Int) = i * 2 
    var b = fun(): Int { return 4 } 
    var c = fun(s: String){ println(s) } 

它也可以反过来。当我们定义属性的类型时,我们不需要在匿名函数中显式设置参数类型,因为它们是从表达式返回类型中推断出来的:

    var a: (Int)->Int = fun(i) = i * 2 
    var c: (String)->Unit = fun(s){ println(s) }

如果我们查看函数类型的方法,那么我们将看到里面只有invoke方法。invoke方法是一个操作符函数,它可以像函数调用一样使用。这就是为什么可以通过在括号内使用invoke调用来实现相同的结果:

    println(a.invoke(4))        // Prints: 8 
    println(b.invoke())         // Prints: 4 
    c.invoke("Hello, World!")   // Prints: Hello, World! 

这种知识在某些情况下是有帮助的,比如当我们将函数保存在可空变量中时。例如,我们可以使用invoke方法通过安全调用:

    var a: ((Int) -> Int)? = null // 1 
    if (false) a = fun(i: Int) = i * 2 
    print(a?.invoke(4)) // Prints: null 
  1. 变量a是可空的,我们使用安全调用来调用。

让我们看一个 Android 示例。我们经常希望定义一个单一的错误处理程序,其中包括多个日志记录方法,并将其作为参数传递给不同的对象。以下是我们可以使用匿名函数来实现它的方式:

    val TAG = "MainActivity" 
    val errorHandler = fun (error: Throwable) { 
        if(BuildConfig.DEBUG) { 
            Log.e(TAG, error.message, error) 
        } 
        toast(error.message) 
        // Other methods, like: Crashlytics.logException(error) 
    } 

    // Usage in project 
    val adController = AdController(errorHandler) 
    val presenter = MainPresenter(errorHandler) 

    // Usage 
    val error = Error("ExampleError") 
    errorHandler(error) // Logs: MainActivity: ExampleError 

匿名函数简单而有用。它们是定义可以用作对象并传递的函数的简单方式。但是有一种更简单的方法可以实现类似的行为,它被称为 lambda 表达式。

Lambda 表达式

在 Kotlin 中定义匿名函数的最简单方式是使用一个称为 lambda 表达式的特性。它们类似于 Java 8 的 lambda 表达式,但最大的区别是 Kotlin 的 lambda 实际上是闭包,因此允许我们从创建上下文更改变量。这在 Java 8 的 lambda 中是不允许的。我们将在本节后面讨论这种差异。让我们从一些简单的示例开始。Kotlin 中的 lambda 表达式具有以下表示法:

    { arguments -> function body } 

返回值是最后一个表达式的结果。以下是一些简单的 lambda 表达式示例:

  • { 1 }:一个 lambda 表达式,不接受参数并返回 1。它的类型是()->Int

  • { s: String -> println(s) }:一个 lambda 表达式,接受一个String类型的参数,并打印它。它返回Unit。它的类型是(String)->Unit

  • { a: Int, b: Int -> a + b }:一个 lambda 表达式,接受两个Int参数并返回它们的和。它的类型是(Int, Int)->Int

我们在上一章中定义的函数可以使用 lambda 表达式来定义:

    var a: (Int) -> Int = { i: Int -> i * 2 } 
    var b: ()->Int = { 4 } 
    var c: (String)->Unit = { s: String -> println(s) } 

虽然 lambda 表达式中的返回值是从最后一个语句中取得的,但是除非有带有标签的return语句,否则是不允许使用return的:

    var a: (Int) -> Int = { i: Int -> return i * 2 } 

    // Error: Return is not allowed there 
    var l: (Int) -> Int = l@ { i: Int -> return@l i * 2 } 

Lambda 表达式可以是多行的:

    val printAndReturn = { i: Int, j: Int -> 
        println("I calculate $i + $j") 
        i + j // 1 
    } 
  1. 这是最后一个语句,因此这个表达式的结果将是一个返回值。

多个语句也可以在一行中定义,当它们用分号分隔时:

val printAndReturn = {i: Int, j: Int -> println("I calculate $i + $j"); 

                      i + j } 

lambda 表达式不仅需要操作由参数提供的值。Kotlin 中的 lambda 表达式可以使用创建它们的上下文中的所有属性和函数:

    val text = "Text" 
    var a: () -> Unit = { println(text) } 
    a() // Prints: Text 
    a() // Prints: Text 

这是 Kotlin 和 Java 8 lambda 使用之间最大的区别。Java 匿名对象和 Java 8 lambda 表达式都允许我们使用上下文中的字段,但 Java 不允许我们为这些变量分配不同的值(lambda 中使用的 Java 变量必须是 final):

Kotlin 通过允许 lambda 表达式和匿名函数修改这些变量迈出了一步。包围局部变量并允许我们在函数体内更改它们的 lambda 表达式称为闭包。Kotlin 完全支持闭包定义。为了避免混淆 lambda 和闭包,在本书中,我们将始终称它们为 lambda。让我们看一个例子:

    var i = 1 
    val a: () -> Int = { ++i } 
    println (i)     // Prints: 1 
    println (a())   // Prints: 2 
    println (i)     // Prints: 2 
    println (a())   // Prints: 3 
    println (i)     // Prints: 3

lambda 表达式可以使用和修改局部上下文中的变量。这是一个计数器的例子,其中值保存在一个局部变量中:

    fun setUpCounter() { 
        var value: Int = 0 
        val showValue = { counterView.text = "$value" } 
        counterIncView.setOnClickListener { value++; showValue() } 

        // 1 
        counterDecView.setOnClickListener { value--; showValue() } 

        // 1 
    } 
  1. 以下是如何在 Kotlin 中使用 lambda 表达式设置 View onClickListener。这将在Java SAM support in Kotlin部分中描述。

由于这个特性,使用 lambda 表达式变得更简单。请注意,在前面的例子中,没有指定showValue的类型。这是因为在 Kotlin lambda 中,当编译器可以从上下文中推断出类型时,参数的类型是可选的:

    val a: (Int) -> Int = { i -> i * 2 }  // 1 
    val c: (String)->Unit = { s -> println(s) } // 2 
  1. i的推断类型是Int,因为函数类型定义了一个Int参数。

  2. s的推断类型是String,因为函数类型定义了一个String参数。

正如我们在下面的例子中看到的,我们不需要指定参数的类型,因为它是从属性的类型中推断出来的。类型推断也可以以另一种方式工作--我们可以定义 lambda 表达式的参数类型以推断属性类型:

    val b = { 4 }                        // 1 
    val c = { s: String -> println(s) }  // 2 
    val a = { i: Int -> i * 2 }          // 3 
  1. 推断的类型是()->Int,因为4Int,并且没有参数类型。

  2. 推断的类型是(String)->Unit,因为参数被定义为String,而println方法的返回类型是Unit

  3. 推断的类型是(Int)->Int,因为i被定义为Int,并且Int的 times 操作的返回类型也是Int

这种推断简化了 lambda 表达式的定义。通常,在我们将 lambda 表达式定义为函数参数时,我们不需要每次指定参数类型。但还有另一个好处--虽然参数类型可以被推断,但可以使用更简单的表示法来表示单个参数的 lambda 表达式。让我们在下一节中讨论这个问题。

单个参数的隐式名称

当满足两个条件时,我们可以省略 lambda 参数定义并使用it关键字访问参数:

  • 只有一个参数

  • 参数类型可以从上下文中推断出来

举个例子,让我们再次定义属性ac,但这次使用单个参数的隐式名称:

    val a: (Int) -> Int = { it * 2 }         // 1
    val c: (String)->Unit = { println(it) }  // 2 
  1. { i -> i * 2 }相同。

  2. { s -> println(s) }相同。

这种表示法在 Kotlin 中非常流行,主要是因为它更短,可以避免参数类型的指定。它还提高了 LINQ 风格中定义的处理的可读性。这种风格需要尚未介绍的组件,但只是为了展示这个想法,让我们看一个例子:

    strings.filter { it.length = 5 }.map { it.toUpperCase() } 

假设 strings 是List<String>,这个表达式会过滤长度等于5的字符串,并将它们转换为大写。

请注意,在 lambda 表达式的主体中,我们可以使用String类的方法。这是因为函数类型(例如filter(String)->Boolean)是从方法定义中推断出来的,它从可迭代类型(List<String>)中推断出String。此外,返回的列表的类型(List<String>)取决于 lambda 返回的内容(String)。

LINQ 风格在函数式语言中很受欢迎,因为它使集合或字符串处理的语法变得非常简单和简洁。它将在第七章 扩展函数和属性中更详细地讨论。

高阶函数

高阶函数是一个至少接受一个函数作为参数或将函数作为其结果返回的函数。在 Kotlin 中,它得到了充分的支持,因为函数是一等公民。让我们在一个例子中看看它。假设我们需要两个函数:一个函数将从列表中添加所有BigDecimal数字,另一个函数将得到所有这些数字的乘积(列表中所有元素之间的乘法结果):

    fun sum(numbers: List<BigDecimal>): BigDecimal { 
        var sum = BigDecimal.ZERO 
        for (num in numbers) { 
            sum += num 
        } 
        return sum 
    } 

    fun prod(numbers: List<BigDecimal>): BigDecimal { 
        var prod = BigDecimal.ONE 
        for (num in numbers) { 
            prod *= num 
        } 
        return prod 
    } 

    // Usage 
    val numbers = listOf( 
        BigDecimal.TEN,  
        BigDecimal.ONE,  
        BigDecimal.valueOf(2) 
    ) 
    print(numbers)          //[10, 1, 2] 
    println(prod(numbers))  // 20 
    println(sum(numbers))   // 13 

这些是可读的函数,但这些函数几乎相同。唯一的区别是名称、累加器(BigDecimal.ZEROBigDecimal.ONE)和操作。如果我们遵循DRY不要重复自己)规则,那么我们不应该在项目中留下两个相似代码的部分。虽然很容易定义一个函数,它将具有类似的行为,只是使用的对象不同,但很难定义一个函数,它将根据执行的操作不同(这里,函数根据用于累加的操作不同)。解决方案是使用函数类型,因为我们可以将操作作为参数传递。在这个例子中,可以这样提取公共方法:

    fun sum(numbers: List<BigDecimal>) = 
        fold(numbers, BigDecimal.ZERO) { acc, num -> acc + num } 

    fun prod(numbers: List<BigDecimal>) = 
       fold(numbers, BigDecimal.ONE) { acc, num -> acc * num } 

    private fun fold( 
        numbers: List<BigDecimal>, 
        start: BigDecimal, 
        accumulator: (BigDecimal, BigDecimal) -> BigDecimal 
    ): BigDecimal { 
        var acc = start 
        for (num in numbers) { 
            acc = accumulator(acc, num) 
        } 
        return acc 
    } 

    // Usage 

    fun BD(i: Long) = BigDecimal.valueOf(i) 
    val numbers = listOf(BD(1), BD(2), BD(3), BD(4)) 
    println(sum(numbers))   // Prints: 10 
    println(prod(numbers))  // Prints: 24 

fold函数遍历数字并使用每个元素更新acc。请注意,函数参数像任何其他类型一样定义,并且可以像任何其他函数一样使用。例如,我们可以有可变参数函数类型参数:

    fun longOperation(vararg observers: ()->Unit) {
        //... 
        for(o in observers) o()
    } 

longOperation中,for用于迭代所有观察者并依次调用它们。这个函数允许提供多个函数作为参数。这里是一个例子:

    longOperation({ notifyMainView() }, { notifyFooterView() })

Kotlin 中的函数也可以返回函数。例如,我们可以定义一个函数,它将创建具有相同错误日志记录但不同标记的自定义错误处理程序:

    fun makeErrorHandler(tag: String) = fun (error: Throwable) { 
        if(BuildConfig.DEBUG) Log.e(tag, error.message, error) 
        toast(error.message) 
        // Other methods, like: Crashlytics.logException(error) 
    } 

    // Usage in project 
    val adController = AdController(makeErrorHandler("Ad in MainActivity")) 
    val presenter = MainPresenter(makeErrorHandler("MainPresenter")) 

    // Usage 
    val exampleHandler = makeErrorHandler("Example Handler") 
    exampleHandler(Error("Some Error")) // Logs: Example Handler: Some Error 

函数参数中使用函数的三种最常见情况是:

  • 为函数提供操作

  • 观察者(监听器)模式

  • 线程操作后的回调

让我们详细看看它们。

为函数提供操作

正如我们在前一节中看到的,有时我们想从函数中提取公共功能,但它们在使用的操作上有所不同。在这种情况下,我们仍然可以提取这个功能,但是我们需要提供一个区分它们的操作参数。这样,任何常见的模式都可以被提取和重用。例如,我们通常只需要列表中与某些谓词匹配的元素,比如当我们只想显示活动元素时。传统上,这样实现:

    var visibleTasks = emptyList<Task>() 
    for (task in tasks) { 
        if (task.active) 
        visibleTasks += task 
    } 

虽然这是一个常见的操作,但我们可以提取仅根据谓词过滤一些元素的功能,以分离函数并更容易使用它:

    fun <T> filter(list: List<T>, predicate: (T)->Boolean) { 
        var visibleTasks = emptyList<T>() 
        for (elem in list) { 
            if (predicate(elem)) 
                visibleTasks += elem 
        } 
    } 

    var visibleTasks = filter(tasks, { it.active }) 

使用高阶函数的这种方式非常重要,并且在整本书中经常会被描述,但这并不是高阶函数经常被使用的唯一方式。

观察者(监听器)模式

当事件发生时,我们使用观察者(监听器)模式来执行操作。在 Android 开发中,观察者经常设置为视图元素。常见的例子是点击监听器、触摸监听器或文本监视器。在 Kotlin 中,我们可以在没有样板文件的情况下设置监听器。例如,设置按钮点击监听器如下所示:

    button.setOnClickListener({ someOperation() }) 

请注意,setOnClickListener是 Android 库中的一个 Java 方法。稍后,我们将详细看到为什么我们可以使用它与 lambda 表达式。监听器的创建非常简单。这是一个例子:

    var listeners: List<()->Unit> = emptyList() // 1 
    fun addListener(listener: ()->Unit) { 
        listeners += listener // 2 
    } 

    fun invokeListeners() { 
        for( listener in listeners) listener() // 3 
    } 
  1. 在这里,我们创建一个空列表来保存所有监听器。

  2. 我们可以简单地将监听器添加到监听器列表中。

  3. 我们可以遍历监听器并依次调用它们。

很难想象有一个更简单的实现方式。还有另一个常见的用例,即在线程操作后常用的参数。

线程操作后的回调

如果我们需要执行一个长时间的操作,并且不想让用户等待,那么我们必须在另一个线程中启动它。为了能够在单独的线程中调用长时间操作后的回调,我们需要将其作为参数传递。这是一个示例函数:

fun longOperationAsync(longOperation: ()->Unit, callback: ()->Unit) { 
        Thread({ // 1 
            longOperation() // 2 
            callback() // 3 
        }).start() // 4 
    } 

    // Usage

    longOperationAsync

(

            longOperation = 

{ 

Thread.sleep(1000L

) }

,

            callback = 

{ 

print

("After second"

) } 

            // 5, Prints: 

After second

    )

    println

("Now"

) // 6, Prints: Now
  1. 在这里,我们创建Thread。我们还传递了一个我们想要在构造函数参数上执行的 lambda 表达式。

  2. 在这里,我们正在执行一个长时间的操作。

  3. 在这里,我们启动了提供的回调操作。

  4. start是启动定义线程的方法。

  5. 在一秒延迟后打印。

  6. 立即打印。

实际上,有一些流行的替代方案可以使用回调,例如 RxJava。不过,经典的回调仍然常用,在 Kotlin 中可以无样板实现。

这些是使用高阶函数的最常见用例。所有这些都允许我们提取公共行为并减少样板文件。Kotlin 允许在高阶函数方面进行一些改进。

命名参数和 lambda 表达式的组合

在 Android 中使用默认命名参数和 lambda 表达式可以非常有用。让我们看一些其他实际的 Android 示例。假设我们有一个函数,它下载元素并将它们显示给用户。我们将添加一些参数:

  • onStart:这将在网络操作之前调用

  • onFinish:这将在网络操作之后调用

fun getAndFillList(onStart: () -> Unit = {}, 

    onFinish: () -> Unit = {}){ 
        // code 
    } 

然后,我们可以在onStartonFinish中显示和隐藏加载旋转器:

    getAndFillList( 
        onStart = { view.loadingProgress = true } , 
        onFinish = { view.loadingProgress = false } 
    ) 

如果我们从swipeRefresh开始,那么当它完成时我们只需要隐藏它:

getAndFillList(onFinish = { view.swipeRefresh.isRefreshing = 

   false }) 

如果我们想进行一个安静的刷新,那么我们只需调用这个:

    getAndFillList() 

命名参数语法和 lambda 表达式是多用途函数的完美匹配。这连接了选择要实现的参数和应该实现的操作的能力。如果一个函数包含多个函数类型参数,那么在大多数情况下,应该使用命名参数语法。这是因为当使用多个 lambda 表达式作为参数时,它们很少是自解释的。

参数约定中的最后一个 lambda

在 Kotlin 中,高阶函数非常重要,因此使它们的使用尽可能舒适也很重要。这就是为什么 Kotlin 引入了一种特殊的约定,使高阶函数更简单和清晰。它的工作方式是:如果最后一个参数是一个函数,那么我们可以在括号外定义 lambda 表达式。让我们看看如果我们将其与longOperationAsync函数一起使用,该函数定义如下:

    fun longOperationAsync(a: Int, callback: ()->Unit) { 
        // ... 
    } 

函数类型在参数的最后位置。这就是为什么我们可以这样执行它:

    longOperationAsync(10) {
        hideProgress() 
    } 

由于最后一个 lambda 在参数约定中,我们可以将 lambda 定位在括号后面。看起来好像它在参数之外。

例如,让我们看看在 Kotlin 中如何在另一个线程中调用代码。在 Kotlin 中启动新线程的标准方式是使用 Kotlin 标准库中的thread函数。它的定义如下:

    public fun thread( 
        start: Boolean = true, 
        isDaemon: Boolean = false, 
        contextClassLoader: ClassLoader? = null, 
        name: String? = null, 
        priority: Int = -1, 
        block: () -> Unit): Thread { 
            // implementation 
        } 

正如我们所看到的,block参数,它接受应该异步调用的操作,位于最后位置。所有其他参数都有默认参数定义。这就是为什么我们可以这样使用thread函数:

    thread { /* code */ } 

thread定义有很多其他参数,我们可以通过使用命名参数语法或依次提供它们来设置它们:

    thread (name = "SomeThread") { /*...*/ } 
    thread (false, false) { /*...*/ } 

参数约定中的最后一个 lambda 是语法糖,但它使使用高阶函数变得更容易。这是这种约定真正产生差异的两种最常见情况:

  • 命名代码周围

  • 使用 LINQ 风格处理数据结构

让我们仔细看看它们。

命名代码环绕

有时我们需要标记代码的某些部分以不同的方式执行。thread函数就是这种情况。我们需要一些代码以异步方式执行,因此我们用从thread函数开始的括号将其包围起来。

    thread {  
        operation1() 
        operation2() 
    } 

从外部看,它看起来好像是由名为thread的块包围的代码的一部分。让我们看另一个例子。假设我们想要记录某个代码块的执行时间。作为辅助,我们将定义addLogs函数,它将与执行时间一起打印日志。我们将以以下方式定义它:

    fun addLogs(tag: String, f: () -> Unit) { 
        println("$tag started") 
        val startTime = System.currentTimeMillis() 
        f() 
        val endTime = System.currentTimeMillis() 
        println("$tag finished. It took " + (endTime - startTime)) 
    } 

以下是该函数的用法:

    addLogs("Some operations") { 
        // Operations we are measuring 
    } 

以下是其执行示例:

    addLogs("Sleeper") { 
        Thread.sleep(1000) 
    } 

在执行前面的代码时,将呈现以下输出:

    Sleeper started 
    Sleeper finished. It took 1001 

打印的毫秒数可能会有所不同。

这种模式在 Kotlin 项目中非常有用,因为一些模式与代码块相关联。例如,在执行至少需要此版本才能工作的功能之前,通常会检查 API 的版本是否在 Android 5.x Lollipop 之后。为了检查它,我们使用了以下条件:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {  
        // Operations 
    } 

但在 Kotlin 中,我们可以以以下方式提取函数:

    fun ifSupportsLolipop(f:()->Unit) { 
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) 

        {  
            f() 
        } 
    } 
    //Usage    

    ifSupportsLollipop { 
        // Operation 
    } 

这不仅舒适,而且还减少了代码中的冗余。这通常被称为非常好的实践。还要注意,这种约定使我们能够定义与标准结构类似工作的控制结构。例如,我们可以定义一个简单的控制结构,只要主体中的语句不返回错误,就会一直运行。以下是定义和用法:

    fun repeatUntilError(code: ()->Unit): Throwable { 
        while (true) { 
            try { 
                code() 
            } catch (t: Throwable) { 
                return t 
            } 
        } 
    } 

    // Usage 
    val tooMuchAttemptsError = repeatUntilError { 
        attemptLogin() 
    } 

额外的优势是我们的自定义数据结构可以返回一个值。令人印象深刻的是,它不需要任何额外的语言支持,我们可以定义几乎任何我们想要的控制结构。

使用 LINQ 风格处理数据结构

我们已经提到 Kotlin 允许 LINQ 风格的处理。参数中的最后一个 lambda 约定是另一个有助于其可读性的组件。例如,看下面的代码:

    strings.filter { it.length == 5 }.map { it.toUpperCase() } 

它比不使用参数中的最后一个 lambda 约定的表示法更易读:

    strings.({ s -> s.length == 5 }).map({ s -> s.toUpperCase() }) 

再次强调,这种处理将在稍后的第七章中详细讨论,扩展函数和属性,但现在我们已经了解了两个改进其可读性的特性(参数中的最后一个 lambda 约定和单个参数的隐式名称)。

参数中的最后一个 lambda 约定是 Kotlin 引入的一个特性,旨在改进 lambda 表达式的使用。还有更多这样的改进,它们如何一起工作对于使高阶函数的使用简单、可读和高效非常重要。

Kotlin 中的 Java SAM 支持

在 Kotlin 中使用高阶函数非常容易。问题在于,我们经常需要与 Java 进行交互,而 Java 本身不支持它。它通过使用只有一个方法的接口来实现替代。这种接口称为单一抽象方法SAM)或功能接口。我们需要以这种方式设置函数的最佳示例是在使用View元素上的setOnClickListener时。在 Java(直到 8)中,没有比使用匿名内部类更简单的方法:

    //Java 
    button.setOnClickListener(new OnClickListener() { 
        @Override public void onClick(View v) { 
            // Operation 
        } 
    }); 

在前面的例子中,OnClickListener方法是 SAM,因为它只包含一个方法onClick。虽然 SAM 经常被用作函数定义的替代,但 Kotlin 也为它们生成了一个包含函数类型作为参数的构造函数。它被称为 SAM 构造函数。SAM 构造函数允许我们通过调用其名称并传递函数文字来创建 Java SAM 接口的实例。以下是一个例子:

    button.setOnClickListener(OnClickListener { 
        /* ... */ 
    }) 

函数文字是定义未命名函数的表达式。在 Kotlin 中,有两种函数文字

  1. 匿名函数

  2. Lambda 表达式

Kotlin function literal 已经被描述过了:

val a = fun() {} // Anonymous function

val b = {} // Lambda expression

更好的是,对于每个接受 SAM 的 Java 方法,Kotlin 编译器都会生成一个接受函数作为参数的版本。这就是为什么我们可以这样设置 OnClickListener

    button.setOnClickListener { 
        // Operations 
    } 

请记住,Kotlin 编译器只为 Java SAM 生成 SAM 构造函数和函数方法。它不会为具有单个方法的 Kotlin 接口生成 SAM 构造函数。这是因为 Kotlin 社区正在推动在 Kotlin 代码中使用函数类型而不是 SAM。当一个函数是用 Kotlin 编写的并包含 SAM 时,我们无法将其用作具有 SAM 参数的 Java 方法:

    interface OnClick { 
        fun call() 
    } 

    fun setOnClick(onClick: OnClick) { 
        //... 
    } 

    setOnClick {  } // 1\. Error 
  1. 这不起作用,因为 setOnClick 函数是用 Kotlin 编写的。

在 Kotlin 中,不应该以这种方式使用接口。首选的方式是使用函数类型而不是 SAM:

    fun setOnClick(onClick: ()->Unit) { 
        //...   
    } 

    setOnClick {  } // Works 

Kotlin 编译器为在 Java 中定义的每个 SAM 接口生成一个 SAM 构造函数。这个接口只包括可以替代 SAM 的函数类型。看看下面的接口:

    // Java, inside View class 
    public interface OnClickListener { 
        void onClick(View v); 
    } 

我们可以在 Kotlin 中这样使用它:

    val onClick = View.OnClickListener { toast("Clicked") } 

或者我们可以将其作为函数参数提供:

    fun addOnClickListener(d: View.OnClickListener) {} 
    addOnClickListener( View.OnClickListener { v -> println(v) }) 

以下是 Java SAM lambda 接口和 Android 方法的更多示例:

    view.setOnLongClickListener { /* ... */; true } 
    view.onFocusChange { view, b -> /* ... */ } 

    val callback = Runnable { /* ... */ } 
    view.postDelayed(callback, 1000) 
    view.removeCallbacks(callback) 

以下是来自 RxJava 的一些示例:

    observable.doOnNext { /* ... */ } 
    observable.doOnEach { /* ... */ } 

现在,让我们看看 Kotlin 中如何实现对 SAM 定义的替代方案。

命名 Kotlin 函数类型

Kotlin 不支持在 Kotlin 中定义的类型的 SAM 转换,因为首选的方式是使用函数类型。但是 SAM 在经典函数类型上有一些优势:命名参数。当函数类型的定义很长或者作为参数传递多次时,最好将函数类型命名。当仅凭类型无法清楚地知道每个参数的含义时,最好使用命名参数。

在接下来的部分中,我们将看到可以为函数类型的参数和整个定义命名。可以通过类型别名和函数类型中的命名参数来实现。这样,可以在坚持使用函数类型的同时获得 SAM 的所有优势。

函数类型中的命名参数

到目前为止,我们只看到了函数类型的定义,其中只指定了类型,而没有指定参数名称。参数名称已在 function literals 中指定:

    fun setOnItemClickListener(listener: (Int, View, View)->Unit) { 
        // code 
    } 
    setOnItemClickListener { position, view, parent -> /* ... */ } 

当参数不是自解释的,开发人员不知道参数的含义时就会出现问题。在 SAM 中有建议,而在前面示例中定义的函数类型中,它们并不真正有帮助:

解决方案是使用带有命名参数的函数类型。以下是它的样子:

    (position: Int, view: View, parent: View)->Unit 

这种表示法的好处是,IDE 建议这些名称作为 function literal 中参数的名称。由于这个原因,程序员可以避免任何混淆:

当同一函数类型多次使用时,定义每个函数类型的参数并不容易。在这种情况下,使用了不同的 Kotlin 功能 - 我们在下一节中描述的 type alias

类型别名

从 1.1 版本开始,Kotlin 具有称为 type alias 的功能,允许我们为现有类型提供替代名称。以下是一个类型别名定义的示例,我们已经制作了一个 Users 的列表:

    data class User(val name: String, val surname: String) 
    typealias Users = List<User> 

这样,我们可以为现有的数据类型添加更有意义的名称:

    typealias Weight = Double 
    typealias Length = Int 

类型别名必须在顶层声明。可以应用可见性修饰符来调整类型别名的范围,但它们默认是公共的。这意味着之前定义的类型别名可以无限制地使用:

    val users: Users = listOf( 
        User("Marcin", "Moskala"),  
        User("Igor", "Wojda") 
    ) 

    fun calculatePrice(length: Length) { 
        // ... 
    } 
    calculatePrice(10) 

    val weight: Weight = 52.0 
    val length: Length = 34 

请记住,别名用于提高代码的可读性,原始类型仍然可以互换使用:

    typealias Length = Int 
    var intLength: Int = 17 
    val length: Length = intLength 
    intLength = length 

typealias的另一个应用是缩短长泛型类型并为其提供更有意义的名称。当相同类型在代码中的多个位置使用时,这可以提高代码的可读性和一致性:

    typealias Dictionary<V> = Map<String, V> 
    typealias Array2D<T> = Array<Array<T>> 

类型别名通常用于命名函数类型:

    typealias Action<T> = (T) -> Unit 
    typealias CustomHandler = (Int, String, Any) -> Unit 

我们可以将它们与函数类型参数名称一起使用:

    typealias OnElementClicked = (position: Int, view: View, parent: View)->Unit 

然后我们得到参数建议:

让我们看一个例子,函数类型由类型别名命名的方法可以通过类实现。在这个例子中,函数类型的参数名称也建议作为方法参数名称:

    typealias OnElementClicked = (position: Int, view: View, parent: View)->Unit 

    class MainActivity: Activity(), OnElementClicked { 

        override fun invoke(position: Int, view: View, parent: View) { 
            // code 
        } 
    } 

这些是我们使用命名函数类型的主要原因:

  • 名称通常比整个函数类型定义更短更容易

  • 当我们传递函数时,在更改其定义后,如果我们使用类型别名,则无需在所有地方都进行更改

  • 当我们使用类型别名时,更容易定义参数名称

这两个特性(函数类型中的命名参数和类型别名)的结合是 Kotlin 中不需要定义 SAM 的原因--所有 SAM 相对于函数类型的优势(名称和命名参数)都可以通过函数类型定义中的命名参数和类型别名来实现。这是 Kotlin 支持函数式编程的另一个例子。

未使用的变量下划线

在某些情况下,我们正在定义一个不使用所有参数的 lambda 表达式。当我们保留它们的名称时,可能会对阅读此 lambda 表达式并尝试理解其目的的程序员进行解构。让我们看一下过滤每个第二个元素的函数。第二个参数是元素值,在这个例子中没有使用:

    list.filterIndexed { index, value -> index % 2 == 0 } 

为了防止误解,有一些惯例,比如忽略参数名称:

    list.filterIndexed { index, ignored -> index % 2 == 0 } 

由于这些约定不清晰且有问题,Kotlin 引入了下划线表示法,用作未使用的参数的名称的替代:

    list.filterIndexed { index, _ -> index % 2 == 0 } 

建议使用此表示法,并在未使用 lambda 表达式参数时显示警告:

在 lambda 表达式中解构

在第四章 类和对象 中,我们已经看到了如何使用解构声明将对象解构为多个属性:

data class User(val name: String, val surname: String, val phone: String) 

val (name, surname, phone) = user 

自 1.1 版本以来,Kotlin 可以使用解构声明语法来进行 lambda 参数。要使用它们,您应该使用包含我们想要解构的所有参数的括号:

    val showUser: (User) -> Unit = { (name, surname, phone) -> 
        println("$name $surname have phone number: $phone")  
    } 

    val user = User("Marcin", "Moskala", "+48 123 456 789") 
    showUser(user) 

    // Marcin Moskala have phone number: +48 123 456 789 

Kotlin 的解构声明是基于位置的,与基于属性名称的解构声明相反,例如在 TypeScript 中可以找到。在基于位置的解构声明中,属性的顺序决定了分配给哪个变量。在基于属性名称的解构中,它由变量的名称决定:

//TypeScript

const obj = { first: 'Jane', last: 'Doe' };

const { last, first } = obj;

console.log(first); // 打印:Jane

console.log(last); // 打印:Doe

这两种解决方案各有优缺点。基于位置的解构声明对于重命名属性是安全的,但对于属性重新排序是不安全的。基于名称的解构声明对于属性重新排序是安全的,但对于属性重命名是脆弱的。

解构声明可以在单个 lambda 表达式中多次使用,并且可以与普通参数一起使用:

    val f1: (Pair<Int, String>)->Unit = { (first, second) -> 

        /* code */ } // 1 
    val f2: (Int, Pair<Int, String>)->Unit = { index, (f, s)-> 

        /* code */ } // 2 
    val f3: (Pair<Int, String>, User) ->Unit = { (f, s), (name, 

        surname, tel) ->/* code */ } // 3 
  1. Pair的解构

  2. Pair和其他元素的解构

  3. 单个 lambda 表达式中的多个解构

请注意,我们可以将类解构为不到所有组件:

    val f: (User)->Unit = { (name, surname) -> /* code */ } 

解构声明中允许使用下划线表示法。它最常用于获取更多的组件:

    val f: (User)->Unit = { (name, _, phone) -> /* code */ } 
    val third: (List<Int>)->Int = { (_, _, third) -> third } 

可以指定解构参数的类型:

    val f = { (name, surname): User -> /* code */ } //1
  1. 类型是从 lambda 表达式中推断出来的

还有解构声明定义的参数:

    val f = { (name: String, surname: String): User -> 

       /* code */}// 1 
    val f: (User)->Unit = { (name, surname) -> 

      /* code */ } // 2 
  1. 类型是从 lambda 表达式中推断出来的。

  2. 由于 lambda 表达式内部的类型信息不足,无法推断类型。

所有这些都使得 lambda 中的解构成为一个非常有用的特性。让我们看一些在 Android 中使用解构在 lambda 中的最常见用例。它用于处理Map的元素,因为它们是Map.Entry类型,可以被解构为keyvalue参数:

    val map = mapOf(1 to 2, 2 to "A") 
    val text = map.map { (key, value) -> "$key: $value" } 
    println(text) // Prints: [1: 2, 2: A] 

同样,成对的列表也可以被解构:

    val listOfPairs = listOf(1 to 2, 2 to "A") 
    val text = listOfPairs.map { (first, second) -> 

        "$first and $second" } 
    println(text) // Prints: [1 and 2, 2 and A] 

解构声明在我们想要简化数据对象处理时也会被使用:

    fun setOnUserClickedListener(listener: (User)->Unit) { 
        listView.setOnItemClickListener { _, _, position, _ -> 
            listener(users[position]) 
        } 
    } 

    setOnUserClickedListener { (name, surname) -> 
        toast("Clicked to $name $surname") 
    } 

这在用于异步处理元素的库中特别有用(例如 RxJava)。它们的函数被设计用于处理单个元素,如果我们想要处理多个元素,那么我们需要将它们打包在PairTriple或其他一些数据类中,并在每一步使用解构声明:

getQuestionAndAnswer() 
    .flatMap { (question, answer) -> 

      view.showCorrectAnswerAnimationObservable(question, answer)  
    }   
    .subscribe( { (question, answer) -> /* code */ } ) 

内联函数

高阶函数非常有用,可以真正提高代码的可重用性。然而,使用它们的最大担忧之一是效率。Lambda 表达式被编译为类(通常是匿名类),在 Java 中对象的创建是一个繁重的操作。我们仍然可以以有效的方式使用高阶函数,同时保持所有的好处,方法是使函数内联。

内联函数的概念相当古老,主要与 C++或 C 有关。当一个函数被标记为内联时,在代码编译期间,编译器将所有函数调用替换为函数的实际体。此外,作为参数提供的 lambda 表达式将被替换为它们的实际体。它们不会被视为函数,而是作为实际的代码。这使得字节码更长,但运行时执行更加高效。后来,我们会看到标准库中几乎所有的高阶函数都被标记为内联。让我们看一个例子。假设我们用inline修饰符标记了printExecutionTime函数:

    inline fun printExecutionTime(f: () -> Unit) { 
        val startTime = System.currentTimeMillis() 
        f() 
        val endTime = System.currentTimeMillis() 
        println("It took " + (endTime - startTime))  
    } 

    fun measureOperation() { 
        printExecutionTime { 
            longOperation() 
        } 
    } 

当我们编译和反编译measureOperation时,我们会发现函数调用被其实际体替换,参数函数调用被 lambda 表达式的体替换:

    fun measureOperation() { 
        val startTime = System.currentTimeMillis() // 1 
        longOperation() // 2 
        val endTime = System.currentTimeMillis() 
        println("It took " + (endTime - startTime)) 
    } 
  1. 来自printExecutionTime的代码被添加到measureOperation函数体中。

  2. 位于 lambda 内部的代码位于其调用处。如果函数多次使用它,那么代码将替换每个调用。

printExecutionTime的体仍然可以在代码中找到。为了使示例更易读,它被跳过了。它被保留在代码中,因为在编译后可能会被使用,例如,如果这段代码被添加到项目中作为库。而且,当被 Kotlin 使用时,这个函数仍然会作为内联函数工作。

虽然不需要为 lambda 表达式创建类,但内联函数可以加速带有函数参数的函数的执行。这种差异非常重要,建议对至少有一个函数参数的所有短函数使用内联修饰符。不幸的是,使用内联修饰符也有其不好的一面。首先,我们已经提到了--生成的字节码更长。这是因为函数调用被函数体替换,而在此体内部的 lambda 调用被函数文字的体替换。此外,内联函数不能是递归的,也不能使用比此 lambda 表达式更严格的可见性修饰符的函数或类。例如,公共内联函数不能使用私有函数。原因是这可能导致代码注入到不能使用它们的函数中。这将导致编译错误。为了防止这种情况发生,Kotlin 不允许在放置它们的 lambda 表达式中使用比 lambda 表达式更不严格的修饰符的元素。这里有一个例子:

    internal fun someFun() {}  
    inline fun inlineFun() { 
        someFun() // ERROR 
    } 

事实上,在 Kotlin 中,如果我们抑制此警告,可以在inline函数中使用更严格可见性的元素,但这是不好的做法,绝不能这样使用:

// Tester1.kt

fun main(args: Array<String>) { a() }

// Tester2.kt

inline fun a() { b() }

private fun b() { print("B") } 这是怎么可能的?对于内部修饰符来说更简单,因为内部修饰符在底层是公共的。对于私有函数,会创建一个额外的access$b函数,它具有public可见性,并且只调用b函数:

public static final void access$b() { b(); }

这种行为只是为了解释为什么在inline函数内有时可以使用较不严格的修饰符(这些情况可以在 Kotlin 1.1 的 Kotlin 标准库中找到)。在项目中,我们应该设计元素,以便不需要使用这样的抑制。

另一个问题不太直观。虽然没有创建 lambda,但我们无法将函数类型的参数传递给另一个函数。这是一个例子:

    fun boo(f: ()->Int) { 
        //...  
    } 

    inline fun foo(f: () -> Int) { 
        boo (f) // ERROR, 1 
    } 

当函数是inline时,它的函数参数不能传递给不是内联的函数。

这不起作用,因为没有创建f参数。它只是被定义为由函数字面值主体替换。这就是为什么它不能作为参数传递给另一个函数。

处理它的最简单方法是将boo函数也设置为内联。然后就可以了。在大多数情况下,我们不能使太多的函数内联。以下是一些原因:

  • inline函数应该用于较小的函数。如果我们正在创建使用其他inline函数的inline函数,那么在编译后可能会生成一个大的结构。这是一个问题,因为编译时间和生成的代码大小都会受到影响。

  • 虽然inline函数不能使用比它们更严格的可见性修饰符的元素,但如果我们想在库中使用它们,这将是一个问题,因为尽可能多的函数应该是私有的,以保护 API。

处理这个问题的最简单方法是将我们想要传递给另一个函数的函数参数设置为noinline

noinline修饰符

noinline是函数类型参数的修饰符。它使特定参数被视为普通函数类型参数(其调用不会被函数字面值主体替换)。让我们看一个noinline的例子:

    fun boo(f: ()->Unit) { 
        //... 
    } 

    inline fun foo(before: ()->Unit, noinline f: () -> Unit) { // 1 
        before() // 2 
        boo (f) // 3 
    } 
  1. 在参数f之前使用noinline注解修饰符。

  2. 之前的函数将被用作参数的 lambda 表达式的主体所替换。

  3. fnoinline,所以可以将其传递给boo函数。

使用noinline修饰符的两个主要原因如下:

  • 当我们需要将特定的 lambda 传递给其他函数时

  • 当我们频繁调用 lambda 并且不希望代码膨胀太多时

请注意,当我们将所有函数参数都设置为noinline时,几乎不会因为将函数设置为内联而获得性能提升。虽然使用inline可能不会有益,但编译器会显示警告。这就是为什么在大多数情况下,只有在有多个函数参数时才使用noinline,并且我们只将其应用于其中一些参数。

非局部返回

具有函数参数的函数可能类似于本地结构(例如循环)。我们已经看到了ifSupportsLolipop函数和repeatUntilError函数。更常见的例子是forEach修饰符。它是for控制结构的替代品,并且依次调用每个元素的参数函数。它的实现方式如下(在 Kotlin 标准库中有一个forEach修饰符,但我们稍后会看到它,因为它包含了尚未介绍的元素):

    fun forEach(list: List<Int>, body: (Int) -> Unit) { 
        for (i in list) body(i) 
    } 

    // Usage 
    val list = listOf(1, 2, 3, 4, 5) 
    forEach(list) { print(it) } // Prints: 12345 

一个大问题是,在这种方式定义的forEach函数内部,我们无法从外部函数返回。例如,我们可以使用for循环来实现maxBounded函数:

    fun maxBounded(list: List<Int>, upperBound: Int, lowerBound: Int): 

    Int { 
        var currentMax = lowerBound 
        for(i in list) { 
            when { 
                i > upperBound -> return upperBound 
                i > currentMax -> currentMax = i 
            } 
        } 
        return currentMax 
    } 

如果我们希望将forEach作为for循环的替代方案,那么应该允许类似的可能性。问题在于,相同的代码,但使用forEach而不是for循环,将无法编译:

原因与代码的编译方式有关。我们已经讨论过,lambda 表达式被编译为包含定义代码的匿名对象的类,而在那里我们无法从maxBounded函数中返回,因为我们处于不同的上下文中。

forEach函数被标记为内联时,我们会遇到一种情况。正如我们已经提到的,这个函数的主体在编译期间会替换其调用,参数中的所有函数都会被其主体替换。因此,在那里使用return修饰符是没有问题的。然后,如果我们将forEach设置为内联,我们可以在 lambda 表达式中使用 return:

    inline fun forEach(list: List<Int>, body: (Int)->Unit) { 
        for(i in list) body(i) 
    } 

    fun maxBounded(list: List<Int>, upperBound: Int, 

        lowerBound: Int): Int { 
        var currentMax = lowerBound 
        forEach(list) { i -> 
            when { 
                i > upperBound -> return upperBound 
                i > currentMax -> currentMax = i 
            } 
        } 
        return currentMax 
    } 

这就是maxBounded函数在 Kotlin 中的编译方式,当它被反编译为 Java 时,代码看起来是这样的(经过一些清理和简化):

    public static final int maxBounded(@NotNull List list, 

    int upperBound, int lowerBound) { 
        int currentMax = lowerBound; 
        Iterator iter = list.iterator(); 

        while(iter.hasNext()) { 
            int i = ((Number)iter.next()).intValue(); 
            if(i > upperBound) { 
                return upperBound; // 1 
            } 

            if(i > currentMax) { 
                currentMax = i; 
            } 
        } 

        return currentMax; 
    } 

在上面的代码中,return很重要--它在 lambda 表达式中被定义,并且从maxBounded函数中返回。

inline函数的 lambda 表达式中使用的return修饰符称为非局部返回。

在 lambda 表达式中标记返回

让我们看一个需要从 lambda 表达式返回而不是从函数返回的情况。我们可以使用标签来实现这一点。以下是使用标签从 lambda 表达式返回的示例:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { // 1 
        for (i in list) body(i) 
    } 

    fun printMessageButNotError(messages: List<String>) { 
        forEach(messages) messageProcessor@ { // 2 
            if (it == "ERROR") return@messageProcessor // 3 
            print(it) 
        } 
    } 

    // Usage 
    val list = listOf("A", "ERROR", "B", "ERROR", "C") 
    processMessageButNotError(list) // Prints: ABC 
  1. 这是forEach函数的通用实现,可以处理任何类型的列表。

  2. 我们为forEach参数中的 lambda 表达式定义标签。

  3. 我们从由标签指定的 lambda 表达式中返回。

另一个 Kotlin 特性是,作为函数参数定义的 lambda 表达式具有一个默认标签,其名称与它们所定义的函数相同。这个标签称为隐式标签。当我们想要从forEach函数中定义的 lambda 表达式返回时,我们可以通过使用return@forEach来实现。让我们看一个例子:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { // 1 
        for (i in list) body(i) 
    } 

    fun processMessageButNotError(messages: List<String>) { 
        forEach(messages) { 
            if (it == "ERROR") return@forEach // 1 
            process(it) 
        } 
    } 

    // Usage 
    val list = listOf("A", "ERROR", "B", "ERROR", "C") 
    processMessageButNotError(list) // Prints: ABC 
  1. 隐式标签名称取自函数名称。

请注意,虽然forEach函数是内联的,我们也可以使用非局部返回来从processMessageButNotError函数中返回:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { 
        for (i in list) body(i) 
    }  

    fun processMessageButNotError(messages: List<String>) { 
        forEach(messages) { 
            if (it == "ERROR") return 
            process(it) 
        } 
    } 

    // Usage 
    val list = listOf("A", "ERROR", "B", "ERROR", "C") 
    processMessageButNotError(list) // Prints: A 

让我们来看一个更复杂的使用非局部返回标签的例子。假设我们有两个forEach循环,一个嵌套在另一个内部。当我们使用隐式标签时,它将从更深层的循环中返回。在我们的例子中,我们可以用它来跳过特定消息的处理:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { // 1 
        for (i in list) body(i)  
    } 

    fun processMessageButNotError(conversations: List<List<String>>) { 
        forEach(conversations) { messages -> 
            forEach(messages) { 
                if (it == "ERROR") return@forEach // 1\. 
                process(it) 
            } 
        } 
    } 

    // Usage 
    val conversations = listOf( 
        listOf("A", "ERROR", "B"),  
        listOf("ERROR", "C"),  
        listOf("D") 
    ) 
    processMessageButNotError(conversations) // ABCD 
  1. 这将从forEach函数中定义的 lambda 中返回,该函数还将消息作为参数。

我们不能使用隐式标签从同一上下文中的另一个 lambda 表达式中返回,因为它被更深层次的隐式标签所遮蔽。

在这些情况下,我们需要使用非局部隐式标签返回。这只允许在内联函数参数中使用。在我们的例子中,当forEach是内联的时,我们可以通过这种方式从函数字面值返回:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { // 1 
        for (i in list) body(i) 
    } 

    fun processMessageButNotError(conversations: List<List<String>>) { 
        forEach(conversations) conv@ { messages -> 
            forEach(messages) { 
                if (it == "ERROR") return@conv // 1\. 
                print(it) 
            } 
        } 
    } 

    // Usage 
    val conversations = listOf( 
        listOf("A", "ERROR", "B"), 
        listOf("ERROR", "C"), 
        listOf("D") 
    ) 
    processMessageButNotError(conversations) // AD 
  1. 这将从在 conversations 上调用的forEach中定义的 lambda 中返回。

我们也可以只使用非局部返回(没有任何标签的返回)来完成处理:

    inline fun <T> forEach(list: List<T>, body: (T) -> Unit) { // 1 
        for (i in list) body(i) 
    } 

    fun processMessageButNotError(conversations: List<List<String>>) { 
        forEach(conversations) { messages -> 
            forEach(messages) { 
                if (it == "ERROR") return // 1\. 
                process(it) 
           } 
        } 
    } 
  1. 这将从processMessageButNotError函数中返回并完成处理。

Crossinline 修饰符

有时,我们需要在内联函数的函数类型参数中,不直接在函数体中使用,而是在另一个执行上下文中使用,比如本地对象或嵌套函数。但是内联函数的标准函数类型参数不允许以这种方式使用,因为它们允许非局部返回,如果这个函数可以在另一个执行上下文中使用,就不应该允许这种情况。为了通知编译器不允许非局部返回,这个参数必须被注释为crossinline。然后它将像我们在inline函数中期望的替换一样起作用,即使它在另一个 lambda 表达式中使用时:

    fun boo(f: () -> Unit) { 
        //... 
    } 

    inline fun foo(crossinline f: () -> Unit) { 
        boo { println("A"); f() } 
    } 

    fun main(args: Array<String>) { 
        foo { println("B") } 
    } 

这将被编译如下:

    fun main(args: Array<String>) { 
        boo { println("A"); println("B") } 
    } 

虽然没有使用函数创建属性,但不可能将crossinline参数传递给另一个函数作为参数:

让我们看一个实际的例子。在 Android 中,我们不需要Context来在应用程序的主线程上执行操作,因为我们可以使用Looper类的getMainLooper静态函数获取主循环。因此,我们可以编写一个顶级函数,允许简单地将线程切换到主线程。为了优化它,我们首先检查当前线程是否不是主线程。当它是时,操作就被调用。当它不是时,我们创建一个在主线程上操作的处理程序,并进行一个后续操作以从那里调用它。为了加快此函数的执行,我们将使runOnUiThread函数内联,但然后为了允许从另一个线程调用操作,我们需要使它crossinline。这是这个描述的函数的实现:

    inline fun runOnUiThread(crossinline action: () -> Unit) { 
        val mainLooper = Looper.getMainLooper() 
        if (Looper.myLooper() == mainLooper) { 
            action() 
        } else { 
            Handler(mainLooper).post { action() } // 1 
        } 
    } 
  1. 我们可以通过crossinline修饰符在 lambda 表达式中运行action

crossinline注解很有用,因为它允许在 lambda 表达式或本地函数的上下文中使用函数类型,同时保持使函数inline的优势(在这种情况下不需要 lambda 创建)。

内联属性

自 Kotlin 1.1 以来,inline修饰符可以用于没有后备字段的属性。它可以应用于单独的访问器,这将导致它们的主体替换使用,或者它可以用于整个属性,这将产生与使两个访问器都是内联的相同结果。让我们创建一个内联属性,用于检查和更改元素的可见性。这是一个实现,其中两个访问器都是内联的:

var viewIsVisible: Boolean 
inline get() = findViewById(R.id.view).visibility == View.VISIBLE 
inline set(value) { 
  findViewById(R.id.view).visibility = if (value) View.VISIBLE 

  else View.GONE 
} 

如果我们将整个属性标注为内联,也可以实现相同的结果:

inline var viewIsVisible: Boolean 
get() = findViewById(R.id.view).visibility == View.VISIBLE 
  set(value) { 
    indViewById(R.id.view).visibility = if (value) View.VISIBLE 

      else View.GONE 
    } 

// Usage 
if (!viewIsVisible) 
viewIsVisible = true 

前面的代码将被编译如下:

if (!(findViewById(R.id.view).getVisibility() == View.VISIBLE)) 

{ 
  findViewById(R.id.view).setVisibility(true?View.VISIBLE:View.GONE); 
} 

这样,我们省略了 setter 和 getter 函数调用,并且应该期望在编译代码大小增加的代价下获得性能改进。尽管如此,对于大多数属性来说,使用inline修饰符应该是有利的。

函数引用

有时,我们想要作为参数传递的函数已经定义为一个单独的函数。然后我们可以只定义带有其调用的 lambda:

    fun isOdd(i: Int) = i % 2 == 1 

    list.filter { isOdd(it) } 

但 Kotlin 也允许我们将函数作为值传递。为了能够将顶级函数用作值,我们需要使用函数引用,它用作双冒号和函数名(::functionName)。这是一个例子,说明它如何用于为filter提供谓词:

    list.filter(::isOdd) 

这是一个例子:

    fun greet(){ 
        print("Hello! ") 
    } 

    fun salute(){ 
        print("Have a nice day ") 
    } 

    val todoList: List<() -> Unit> = listOf(::greet, ::salute) 

    for (task in todoList) { 
        task()  
    } 

    // Prints: Hello! Have a nice day 

函数引用是反射的一个例子,这就是为什么这个操作返回的对象也包含有关所引用函数的信息:

    fun isOdd(i: Int) = i % 2 == 1 

    val annotations = ::isOdd.annotations 
    val parameters = ::isOdd.parameters   
    println(annotations.size) // Prints: 0 
    println(parameters.size) // Prints: 1 

但这个对象也实现了函数类型,可以这样使用:

    val predicate: (Int)->Boolean = ::isOdd 

还可以引用方法。要这样做,我们需要写类型名称,两个冒号和方法名(Type::functionName)。这是一个例子:

    val isStringEmpty: (String)->Boolean = String::isEmpty 

    // Usage 
    val nonEmpty = listOf("A", "", "B", "") 
    .filter(String::isNotEmpty) 
    print(nonEmpty) // Prints: ["A", "B"] 

与前面的例子类似,当我们引用非静态方法时,需要提供类的实例作为参数。isEmpty函数是一个不带参数的String方法。对isEmpty的引用有一个String参数,该参数将被用作调用该函数的对象。对象的引用总是位于第一个参数。这里是另一个例子,其中方法已经定义了属性food

    class User { 

        fun wantToEat(food: Food): Boolean { 
            // ... 
        } 
    } 

    val func: (User, Food) -> Boolean = User::wantToEat 

当我们引用 Java 静态方法时,情况就不同了,因为它不需要定义它所在的类的实例。这类似于对象伴生对象的方法,其中对象是预先知道的,不需要提供。在这些情况下,有一个与被引用函数相同参数和相同返回类型的函数被创建:

    object MathHelpers { 
        fun isEven(i: Int) = i % 2 == 0 
    } 

    class Math { 
        companion object { 
            fun isOdd(i: Int) = i % 2 == 1 
        } 
    } 

    // Usage 
    val evenPredicate: (Int)->Boolean = MathHelpers::isEven 
    val oddPredicate: (Int)->Boolean = Math.Companion::isOdd 

    val numbers = 1..10 
    val even = numbers.filter(evenPredicate) 
    val odd = numbers.filter(oddPredicate) 
    println(even) // Prints: [2, 4, 6, 8, 10] 
    println(odd) // Prints: [1, 3, 5, 7, 9] 

在函数引用的使用中,有一些常见的用例,我们希望使用函数引用来提供对我们引用的类的方法。常见的例子是当我们想要将一些操作提取为同一类的方法时,或者当我们想要引用来自我们引用的类的引用成员函数的函数时。一个简单的例子是当我们定义网络操作之后应该做什么。它是在一个 Presenter(比如MainPresenter)中定义的,但它引用了所有的 View 操作,这些操作由view属性定义(例如,类型为MainView):

    getUsers().smartSubscribe ( 
        onStart = { view.showProgress() }, // 1 
        onNext = { user -> onUsersLoaded(user) }, // 2 
        onError = { view.displayError(it) }, // 1 
        onFinish = { view.hideProgress() } // 1 
    ) 
  1. showProgressdisplayErrorhideProgressMainView中定义。

  2. onUsersLoaded是在MainPresenter中定义的方法。

为了帮助这种情况,Kotlin 在 1.1 版本中引入了一个叫做bound references的功能,它提供了绑定到特定对象的引用。由于这个,这个对象不需要通过参数提供。使用这种表示法,我们可以用以下方式替换之前的定义:

    getUsers().smartSubscribe ( 
        onStart = view::showProgress, 
        onNext = this::onUsersLoaded, 
        onError = view::displayError, 
        onFinish = view::hideProgress 
    ) 

我们可能想要引用的另一个函数是构造函数。一个例子是当我们需要从数据传输对象DTO)映射到模型中的类时:

    fun toUsers(usersDto: List<UserDto>) = usersDto.map { User(it) } 

在这里,User需要有一个构造函数,定义了它如何从UserDto构造。

DTO 是在进程之间传递数据的对象。它被使用是因为在系统之间的通信中使用的类(在 API 中)与系统内部使用的实际类(模型)不同。

在 Kotlin 中,构造函数的使用和处理类似于函数。我们也可以用双冒号和类名引用它们:

    val mapper: (UserDto)->User = ::User 

这样,我们可以用构造函数引用替换 lambda 的构造函数调用:

    fun toUsers(usersDto: List<UserDto>) = usersDto.map(::User) 

使用函数引用而不是 lambda 表达式给我们提供了更短和更易读的表示法。当我们传递多个函数作为参数,或者函数很长需要被提取时,这种表示法尤其有用。在其他情况下,有一个有用的 bounded reference,它提供了一个绑定到特定对象的引用。

总结

在本章中,我们讨论了将函数作为一等公民。我们已经看到了函数类型是如何被使用的。我们已经看到了如何定义函数字面量(匿名函数和 lambda 表达式),以及任何函数都可以作为对象使用,这要归功于函数引用。我们还讨论了高阶函数和不同的 Kotlin 特性来支持它们:单个参数的隐式名称、参数中的最后一个 lambda、Java SAM 支持、使用下划线表示未使用的变量,以及 lambda 表达式中的解构声明。这些特性为高阶函数提供了很好的支持,使函数不仅仅是一等公民。

在下一章中,我们将看到 Kotlin 中泛型是如何工作的。这将使我们能够定义更强大的类和函数。我们还将看到当与高阶函数连接时它们可以如何被使用。

第六章:泛型是你的朋友

在上一章中,我们讨论了与 Kotlin 中的函数式编程和函数作为一等公民相关的概念。

在本章中,我们将讨论泛型类型和泛型函数的概念,称为泛型。我们将学习它们存在的原因以及如何使用它们-我们将定义泛型类、接口和函数。我们将讨论如何在运行时处理泛型,看一下子类型关系,并处理泛型可空性

在本章中,我们将讨论泛型类型和泛型函数的概念,称为泛型。我们将学习它们存在的原因以及如何使用它们,以及如何定义泛型类、接口和函数。我们将讨论如何在运行时处理泛型,看一下子类型关系,并处理泛型可空性。

在本章中,我们将涵盖以下主题:

  • 泛型类

  • 泛型接口

  • 泛型函数

  • 泛型约束

  • 泛型可空性

  • 变异

  • 使用地点目标与声明地点目标

  • 声明地点目标

  • 类型擦除

  • 具体化和擦除类型参数

  • 星投影语法

  • 变异

泛型

泛型是一种编程风格,其中类、函数、数据结构或算法以后可以指定确切的类型。通常,泛型提供类型安全性以及重用特定代码结构的能力,用于各种数据类型。

泛型在 Java 和 Kotlin 中都存在。它们的工作方式类似,但 Kotlin 在 Java 泛型类型系统上提供了一些改进,比如使用地点变异、星投影语法和具体化类型参数。我们将在本章讨论它们。

泛型的需求

程序员经常需要一种方法来指定集合只包含特定类型的元素,比如IntStudentCar。如果没有泛型,我们将需要为每种数据类型创建单独的类(IntListStudentListCarList等)。这些类的内部实现非常相似,唯一的区别在于存储的数据类型。这意味着我们需要多次编写相同的代码(比如向集合添加或删除项目)并分别维护每个类。这是很多工作,所以在实现泛型之前,程序员通常操作通用列表。这迫使他们每次访问时都需要转换元素:

    // Java

    ArrayList list = new ArrayList();

    list.add(1);

    list.add(2);

    int first = (int) list.get(0);

    int second = (int) list.get(1);

转换会增加样板代码,并且在将元素添加到集合时没有类型验证。泛型是这个问题的解决方案,因为泛型类定义并使用占位符而不是真实类型。这个占位符称为类型参数。让我们定义我们的第一个泛型类:

    class SimpleList<T> // T is type parameter

类型参数意味着我们的类将使用特定类型,但这种类型将在类创建期间指定。这样,我们的SimpleList类可以为各种类型实例化。我们可以使用类型参数为泛型类参数化各种数据类型。这允许我们从单个类创建多个数据类型:

     // Usage

    var intList: SimpleList<Int>

    var studentList: SimpleList<Student>

    var carList:SimpleList<Car>

SimpleList类是使用类型参数IntStudentCar)进行参数化的,定义了可以存储在给定列表中的数据类型。

类型参数与类型参数

函数具有参数(在函数声明内部声明的变量)和参数(传递给函数的实际值)。泛型也适用类似的术语。类型参数是泛型中声明的类型的蓝图或占位符,类型参数是用于参数化泛型的实际类型。

我们可以在方法签名中使用类型参数。这样,我们可以确保我们将能够向我们的列表添加特定类型的项目并检索特定类型的项目:

    class SimpleList<T> { 

       fun add(item:T) { // 1 
           // code 
       }  
       fun get(intex: Int): T { // 2 
           // code 
       } 
    } 
  1. 泛型类型参数T用作项目类型

  2. 类型参数用作返回类型

可以添加到列表或从列表中检索的项目的类型取决于类型参数。让我们看一个例子:

    class Student(val name: String)

    val studentList = SimpleList<Student>()

    studentList.add(Student("Ted"))

    println(studentList.getItemAt(0).name)

我们只能从列表中添加和获取Student类型的项目。编译器将自动执行所有必要的类型检查。可以保证集合只包含特定类型的对象。将不兼容类型的对象传递给 add 方法将导致编译时错误:

    var studentList: SimpleList<Student>

    studentList.add(Student("Ted"))

    studentList.add(true) // error

我们无法添加布尔值,因为期望的类型是Student

Kotlin 标准库在kotlin.collections包中定义了各种通用集合,如ListSetMap。我们将在第七章中讨论它们,扩展函数和属性

在 Kotlin 中,通常将通用与高阶函数(在第五章中讨论)和扩展函数(我们将在第七章中讨论)结合使用。这些连接的示例是函数:mapfiltertakeUntil等。我们可以执行通用操作,其细节将有所不同。例如,我们可以使用filter函数在集合中查找匹配的元素,并指定如何检测匹配的元素:

    val fruits = listOf("Babana", "Orange", "Apple", "Blueberry") 
    val bFruits = fruits.filter { it.startsWith("B") } //1 
    println(bFruits) // Prints: [Babana, Blueberry] 
  1. 我们可以调用startsWith方法,因为集合只能包含Strings,所以 lambda 参数(it)具有相同的类型。

通用约束

默认情况下,我们可以使用任何类型的类型参数对通用类进行参数化。但是,我们可以限制可以用作类型参数的可能类型。为了限制类型参数的可能值,我们需要定义一个类型参数边界。最常见的约束类型是上界。默认情况下,所有类型参数都具有Any?作为隐式上界。这就是为什么以下两个声明是等价的:

    class SimpleList<T>

    class SimpleList<T: Any?>

前面的边界意味着我们可以使用任何类型作为SimpleList类的类型参数(包括可空类型)。这是可能的,因为所有可空和非可空类型都是Any?的子类型:

    class SimpleList<T>

    class Student

    //usage

    var intList = SimpleList<Int>()

    var studentList = SimpleList<Student>()

    var carList = SimpleList<Boolean>()

在某些情况下,我们希望限制可用作类型参数的数据类型。为了实现这一点,我们需要明确定义一个类型参数上界。假设我们只想能够将数值类型用作SimpleList类的类型参数

    class SimpleList<T: Number>

    //usage

    var numberList = SimpleList<Number>()

    var intList = SimpleList<Int>()

    var doubleList = SimpleList<Double>()

    var stringList = SimpleList<String>() //error

Number类是一个抽象类,即 Kotlin 数值类型(ByteShortIntLongFloat,和Double)的超类。我们可以使用Number类及其所有子类(IntDouble等)作为类型参数,但我们不能使用String类,因为它不是Number的子类。任何尝试添加不兼容类型的操作都将被 IDE 和编译器拒绝。类型参数还包括 Kotlin 类型系统的可空性。

可空性

当我们定义一个具有无界类型参数的类时,我们可以使用非可空和可空类型作为类型参数。偶尔,我们需要确保特定的通用类型不会被参数化为可空类型参数。为了阻止使用可空类型作为类型参数的能力,我们需要明确定义一个非可空类型参数上界

    class Action (val name:String)

    class ActionGroup<T : Action> 

    // non-nullable type parameter upper bound

    var actionGroupA: ActionGroup<Action>

    var actionGroupB: ActionGroup<Action?> // Error

现在我们无法将可空的类型参数Action?)传递给ActionGroup类。

让我们考虑另一个例子。假设我们想要检索ActionGroup中的最后一个Actionlast方法的简单定义如下:

    class ActionGroup<T : Action>(private val list: List<T>) {

        fun last(): T = list.last()

    }

让我们分析当我们将空列表传递给构造函数时会发生什么:

    val actionGroup = ActionGroup<Action>(listOf())

    //...

    val action = actionGroup.last 

    //error: NoSuchElementException: List is empty

    println(action.name)

我们的应用程序崩溃了,因为当列表为空时,last方法会抛出错误。我们可能更喜欢在列表为空时返回空值而不是异常。Kotlin 标准库已经有了一个相应的方法,它将返回一个空值:

    class ActionGroup<T : Action>(private val list: List<T>) {

        fun lastOrNull(): T = list.lastOrNull() //error

    }

代码将无法编译,因为最后一个方法可能会返回 null,无论类型参数是否为空(列表中可能没有元素返回)。为了解决这个问题,我们需要通过在类型参数使用位置(T?)添加一个问号来强制可空返回类型:

    class ActionGroup<T : Action>(private val list: List<T>) { // 1

        fun lastOrNull(): T? = list.lastOrNull() // 2

    }
  1. 类型参数声明位置(类型参数声明的代码位置)

  2. 类型参数使用位置(类型参数使用的代码位置)

T?参数意味着lastOrNull方法始终是可空的,无论潜在的类型参数是否为空。请注意,我们将类型参数T的界限恢复为非空类型Action,因为我们希望存储非空类型并仅在某些情况下处理可空性(例如不存在的最后一个元素)。让我们使用我们更新的ActionGroup类:

    val actionGroup= ActionGroup<Action>(listOf())

    val actionGroup = actionGroup.lastOrNull() 

    // Inferred type is Action?

    println(actionGroup?.name) // Prints: null

请注意,即使我们使用非空类型参数对泛型进行参数化,actionGroup推断类型仍然是可空的。

使用位置的可空类型不会阻止我们在声明位置允许非空类型:

    open class Action

    class ActionGroup<T : Action?>(private val list: List<T>) {

        fun lastOrNull(): T? = list.lastOrNull()

    }

    // Usage

    val actionGroup = ActionGroup(listOf(Action(), null))

    println(actionGroup.lastOrNull()) // Prints: null

让我们总结一下上面的解决方案。我们为类型参数指定了一个非空界限,以阻止使用可空类型作为类型参数ActionGroup类进行参数化。我们使用非空类型参数ActionActionGroup类进行参数化。最后,我们在使用位置(T?)强制类型参数的可空性,因为如果列表中没有元素,最后一个属性可能会返回 null。

变化

子类型是面向对象编程范式中的一个流行概念。我们通过扩展类来定义两个类之间的继承:

    open class Animal(val name: String)

    class Dog(name: String): Animal(name)

Dog扩展了类Animal,因此类型DogAnimal的子类型。这意味着我们可以在需要Animal类型的表达式中使用Dog类型的表达式;例如,我们可以将其用作函数参数或将Dog类型的变量分配给Animal类型的变量:

    fun present(animal: Animal) {

        println( "This is ${ animal. name } " )

    }

    present(Dog( "Pluto" )) // Prints: This is Pluto

在我们继续之前,我们需要讨论类和类型之间的区别。类型是一个更一般的术语--它可以由类或接口定义,也可以内置到语言中(原始类型)。在 Kotlin 中,对于每个类(例如Dog),我们至少有两种可能的类型--非空(Dog)和可空(Dog?)。而且,对于每个泛型类(例如class Box<T>),我们可以定义多个数据类型(Box*<Dog>*Box<Dog?>Box<Animal>Box<Box<Dog>>等)。

前面的例子仅适用于简单类型。变化指定了更复杂类型之间的子类型关系(例如Box<Dog>Box<Animal>)与它们的组件之间的子类型关系(例如AnimalDog)之间的关系。

在 Kotlin 中,泛型默认是不变的。这意味着泛型类型Box<Dog>Box<Animal>之间没有子类型关系。DogAnimal的子类型,但Box<Dog>既不是Box<Animal>的子类型也不是超类型:

    class Box<T>

    open class Animal

    class Dog : Animal()

    var animalBox = Box<Animal>()

    var dogBox = Box<Dog>()

    //one of the lines below line must be commented out,

    //otherwise Android Studio will show only one error

    animalBox = dogBox // 2, error

    dogBox = animalBox // 1, error
  1. 错误类型不匹配。需要Box<Animal>,找到Box<Dog>

  2. 错误类型不匹配。需要Box<Dog>,找到Box<Animal>

Box<Dog>类型既不是Box<Animal>的子类型也不是超类型,因此我们不能使用前面代码中显示的任何赋值。

我们可以定义Box<Dog>Box<Animal>之间的子类型关系。在 Kotlin 中,泛型类型的子类型关系可以被保留(协变)、颠倒(逆变)或忽略(不变)。

当子类型关系是协变时,这意味着子类型关系被保留。泛型类型将与类型参数具有相同的关系。如果DogAnimal的子类型,则Box<Dog>Box<Animal>的子类型。

逆变是协变的确切相反,子类型关系被颠倒。泛型类型将与类型参数的关系相反。如果DogAnimal的子类型,则Box<Animal>Box<Dog>的子类型。以下图表显示了所有类型的变化:

要定义协变或逆变行为,我们需要使用变异修饰符

变异修饰符

Kotlin 中的泛型默认是不变的。这意味着我们需要将类型用作声明的变量或函数参数的类型:

    public class Box<T> { }

    fun sum(list: Box<Number>) { /* ... */ }

    // Usage

    sum(Box<Any>()) // Error

    sum(Box<Number>()) // Ok

    sum(Box<Int>()) // Error

我们不能使用泛型类型参数化为IntNumber的子类型)和AnyNumber的超类型)。我们可以通过使用变异修饰符来放宽这个限制并改变默认的变异。在 Java 中,有一个问号(?)符号(通配符符号)用于表示未知类型。使用它,我们可以定义两种通配符边界--上界和下界。在 Kotlin 中,我们可以使用inout修饰符来实现类似的行为。

在 Java 中,上界通配符允许我们定义一个接受任何参数的函数,该参数是其子类型的某种类型。在下面的例子中,sum 函数将接受任何使用Number类或Number类的子类型(Box<Integer>Box<Double>等)参数化的List

    //Java

    public void sum(Box<? extends Number> list) { /* ... */ }

    // Usage

    sum(new Box<Any>()) // Error

    sum(new Box<Number>()) // Ok

    sum(new Box<Int>()) // Ok

现在我们可以将Box<Number>传递给我们的 sum 函数以及所有的子类型,例如Box<Int>。这种 Java 行为对应于 Kotlin 的 out 修饰符。它表示协变,限制类型为特定类型或该类型的子类型。这意味着我们可以安全地传递使用任何Number的直接或间接子类参数化的Box类的实例:

    class Box<T>

    fun sum(list: Box<out Number>) { /* ... */ }

    //usage

    sum(Box<Any>()) // Error

    sum(Box<Number>()) // Ok

    sum(Box<Int>()) // Ok

在 Java 中,下界通配符允许我们定义一个接受任何参数的函数,该参数是某种类型或其超类型。在下面的例子中,sum函数将接受任何使用Number类或Number类的超类型(Box<Number>Box<Object>)参数化的List

    //Java

    public void sum(Box<? super Number> list) { /* ... */ }

    //usage

    sum(new Box<Any>()) // Ok

    sum(new Box<Number>()) // Ok

    sum(new Box<Int>()) // Error

现在我们可以将Box<Any>传递给我们的 sum 函数以及所有的子类型,例如Box<Any>。这种 Java 行为对应于 Kotlin 的 in 修饰符。它表示逆变,限制类型为特定类型或该类型的超类型:

    class Box<T>

    fun sum(list: Box<in Number>) { /* ... */ }

    //usage

    sum(Box<Any>()) // Ok

    sum(Box<Number>()) // Ok

    sum(Box<Int>()) // Error

禁止同时使用inout修饰符。我们可以以两种不同的方式定义变异修饰符。让我们在接下来的部分看看它们。

使用地点变异与声明地点变异

使用地点变异和声明地点变异基本上描述了代码中指定变异修饰符的位置。让我们考虑ViewPresenter的例子:

    interface BaseView

    interface ProductView : BaseView

    class Presenter<T>

    // Usage

    var preseter = Presenter<BaseView>()

    var productPresenter = Presenter<ProductView>()

    preseter = productPresenter

    // Error: Type mismatch

    // Required: Presenter<BaseView>

    // Found: Presenter<ProductView>

Presenter在其类型parameterT上是不变的。为了解决问题,我们可以明确定义子类型关系。我们可以以两种方式(使用地点和声明地点)进行定义。首先,让我们在使用地点定义变异:

    var preseter: Presenter<out BaseView> = Presenter<BaseView>() //1

    var productPresenter = Presenter<ProductView>()

    preseter = productPresenter
  1. 类型参数使用地点定义的变异修饰符

现在preseter变量可以存储Presenter<BaseView>的子类型,包括Presenter<ProductView>。我们的解决方案有效,但实现可以改进。这种方法有两个问题。现在我们需要每次在想要使用泛型类型时指定out变异修饰符,例如,在不同类中的多个变量中使用它:

    //Variable declared inside class A and class B

    var preseter = Presenter<BaseView>()

    var preseter: Presenter<out BaseView> = Presenter<ProductView>()

    preseter = productPresenter  

AB都包含具有变异修饰符的preseter变量。我们失去了使用类型推断的能力,结果代码更冗长。为了改进我们的代码,我们可以在类型参数声明地点指定变异修饰符:

interface BaseView

interface ProductView: BaseView

class Presenter<out T> // 1   

//usage

//Variable declared inside class A and B

var preseter = Presenter<BaseView>()

var productPresenter = Presenter<ProductView>()

preseter = productPresenter
  1. 在类型参数声明地点定义的变异修饰符

我们只需要在Presenter类内部定义一次变异修饰符。实际上,前面的两种实现是等价的,尽管声明地点变异更简洁,外部类的使用更容易

集合变异

在 Java 中,数组是协变的。默认情况下,我们可以传递一个String[]数组,即使期望的是Object[]数组:

    public class Computer {

        public Computer() {

            String[] stringArray = new String[]{"a", "b", "c"};

            printArray(stringArray); //Pass instance of String[]

        }

        void printArray(Object[] array) { 

            //Define parameter of type Object[]

            System.out.print(array);

        }

    }

这种行为在 Java 的早期版本中很重要,因为它允许我们使用不同类型的数组作为参数:

    // Java

    static void print(Object[] array) {

        for (int i = 0; i <= array.length - 1; i++)

        System.out.print(array[i] + " ");

        System.out.println();

    }

    // Usage

    String[] fruits = new String[] {"Pineapple","Apple", "Orange", 

                                    "Banana"};

    print(fruits); // Prints: Pineapple Apple Orange Banana

    Arrays.sort(fruits);

    print(fruits); // Prints: Apple Banana Orange Pineapple

但这种行为也可能导致潜在的运行时错误:

    public class Computer {

        public Computer() {

            Number[] numberArray = new Number[]{1, 2, 3};

            updateArray(numberArray);

        }

        void updateArray(Object[] array) {

            array[0] = "abc"; 

            // Error, java.lang.ArrayStoreException: java.lang.String

        }

    }

函数updateArray接受类型为Object[]的参数,我们正在传递String[]。我们正在使用String参数调用 add 方法。我们可以这样做,因为数组项的类型是Object,所以我们可以使用String,这是一个新值。最后,我们想要将String添加到可能只包含String类型项的通用数组中。由于默认的协变行为,编译器无法检测到这个问题,这将导致ArrayStoreException异常。

相应的代码在 Kotlin 中不会编译,因为 Kotlin 编译器将此行为视为潜在的危险。这就是为什么 Kotlin 中的数组是不变的原因。因此,在需要Array<Any>时传递除Array<Number>之外的类型将导致编译时错误:

    public class Array<T> { /*...*/ }

因此,在需要Array<Any>时传递除Array<Number>之外的类型将导致编译时错误:

    public class Array<T> { /*...*/ }

    class Computer {

        init {

            val numberArray = arrayOf<Number>(1, 2, 3)

            updateArray(numberArray)

        }

        internal fun updateArray(array: Array<Any>) {

            array[0] = "abc" 

            //error, java.lang.ArrayStoreException: java.lang.String

        }

    }

请注意,只有当我们可以修改对象时才可能发生潜在的运行时异常。变异也适用于 Kotlin 集合接口。在 Kotlin 标准库中,我们有两个以不同方式定义的列表接口。Kotlin List接口被定义为协变,因为它是不可变的(它不包含任何允许我们更改内部状态的方法),而 Kotlin MutableList接口是不变的。以下是它们类型参数的定义:

    interface List<out E> : Collection<E> { /*...*/ }

    public interface MutableList<E> : List<E>, MutableCollection<E> {     

        /*...*/ 

    }

让我们看看这些定义在实际中的后果。它使可变列表免受协变的风险:

    fun addElement(mutableList: MutableList<Any>) {

        mutableList.add("Cat")

    }

    // Usage

    val mutableIntList = mutableListOf(1, 2, 3, 4)

    val mutableAnyList = mutableListOf<Any>(1, 'A')

    addElement(mutableIntList) // Error: Type mismatch

    addElement(mutableAnyList)

该列表是安全的,因为它没有用于更改其内部状态的方法,并且其协变行为允许更广泛地使用函数:

    fun printElements(list: List<Any>) {

        for(e in list) print(e)

    }

    // Usage

    val intList = listOf(1, 2, 3, 4)

    val anyList = listOf<Any>(1, 'A')

    printElements(intList) // Prints: 1234

    printElements(anyList) // Prints: 1A

我们可以将List<Any>或其任何子类型传递给printElements函数,因为List接口是协变的。我们只能将MutableList<Any>传递给addElement函数,因为MutableList接口是不变的。

使用inout修饰符,我们可以操纵变异行为。我们还应该意识到变异有一些限制。让我们讨论一下。

变异生产者/消费者限制

通过应用变异修饰符,我们可以为类/接口的某个类型参数(声明位置变异)或类型参数(使用位置变异)获得协变/逆变行为。然而,我们需要注意一个限制。为了使其安全,Kotlin 编译器限制了类型参数可以使用的位置。

对于不变(类型参数上默认没有变异修饰符),我们可以在in(函数参数的类型)和out(函数返回类型)位置上使用类型参数:

    interface Stack<T> {

        fun push(t:T) // Generic type at in position

        fun pop():T // Generic type at out position

        fun swap(t:T):T // Generic type at in and out positions

        val last: T // Generic type at out position

        var special: T // Generic type at out position

    }

通过变异修饰符,我们只限于一个位置。这意味着我们只能将类型参数用作方法参数的类型(in)或方法返回值(out)。我们的类可以是生产者或消费者,但永远不会同时。我们可以说该类接收参数提供参数

让我们看看这种限制与在声明位置指定的变异修饰符的关系。以下是两个类型参数RT的所有正确和不正确的用法:

    class ConsumerProducer<in T, out R> {

        fun consumeItemT(t: T): Unit { } // 1

        fun consumeItemR(r: R): Unit { } // 2, error

        fun produceItemT(): T { // 3, error

            // Return instance of type T

        }

        fun produceItemR(): R { // 4

            //Return instance of type R

        }

    }
  1. 在 in 位置的 OK 类型参数T

  2. 错误,类型参数R在 in 位置

  3. 错误,类型参数T在 out 位置

  4. 在 out 位置的 OK,类型参数R

正如我们所看到的,如果配置被禁止,编译器将报告错误。请注意,我们可以为两个类型参数RT添加不同的修饰符。

位置限制仅适用于类外可访问(可见)的方法。这意味着不仅仅是所有public方法(public是默认修饰符),如之前所使用的,还包括标记为protectedinternal的方法。当我们将方法可见性更改为private时,我们可以在任何位置使用我们的类型参数(RT),就像不变的类型参数一样:

    class ConsumerProducer<in T, out R> {

        private fun consumeItemT(t: T): Unit { }

        private fun consumeItemR(r: R): Unit { }

        private fun produceItemT(): T {

            // Return instance of type T

        }

        private fun produceItemR(): R {

            //Return instance of type R

        }

    }

让我们看一下下表,它展示了类型参数作为类型使用时的所有允许位置:

可见性修饰符 不变性 协变性(out) 逆变性(in)
publicprotectedinternal in/out out in
private in/out in/out in/out

不变的构造函数

在前一节描述的inout位置规则中,构造函数参数始终是不变的一个重要例外:

    class Producer<out T>(t: T)

    // Usage

    val stringProducer = Producer("A")

    val anyProducer: Producer<Any> = stringProducer

构造函数是公共的,类型参数T声明为out,但我们仍然可以在 in 位置使用它作为构造函数参数类型。原因是构造函数方法在实例创建后不能被调用,因此始终可以安全地调用它。

正如我们在第四章中讨论的那样,类和对象,我们还可以使用valvar修饰符直接在类构造函数中定义属性。当指定协变时,我们只能在构造函数中定义具有协变类型的只读属性(val)。这是安全的,因为只会生成 getter,因此在类实例化后,此属性的值不会改变:

    class Producer<out T>(val t: T) // Ok, safe

使用var时,编译器会生成 getter 和 setter,因此属性值可能在某个时刻发生变化。这就是为什么我们不能在构造函数中声明一个协变类型的读写(var)属性的原因:

    class Producer<out T>(var t: T) // Error, not safe

我们已经说过,变异限制只适用于外部客户端,因此我们仍然可以通过添加私有可见性修饰符来定义一个协变读写属性:

    class Producer<out T>(private var t:T)

另一个来自 Java 的流行的泛型类型限制与类型擦除有关。

类型擦除

类型擦除是引入到 JVM 中的,以使 JVM 字节码向后兼容于引入泛型之前的版本。在 Android 平台上,Kotlin 和 Java 都被编译成 JVM 字节码,因此它们都容易受到类型擦除的影响。

类型擦除是从泛型类型中移除类型参数的过程,因此泛型类型在运行时失去了一些类型信息(类型参数):

    package test

    class Box<T>

    val intBox = Box<Int>()

    val stringBox = Box<String>()

    println(intBox.javaClass) // prints: test.Box

    println(stringBox.javaClass) // prints: test.Box

编译器可以区分两种类型并保证类型安全。然而,在编译过程中,编译器将参数化类型Box<Int>Box<String>转换为Box(原始类型)。生成的 Java 字节码不包含任何与类型参数相关的信息,因此我们无法在运行时区分泛型类型。

类型擦除导致了一些问题。在 JVM 中,我们不能声明具有相同 JVM 签名的同一方法的两个重载:

    /*

    java.lang.ClassFormatError: Duplicate method name&signature...

    */

    fun sum(ints: List<Int>) {

        println("Ints")

    }

    fun sum(strings: List<String>) {

        println("Ints")

    }

类型参数被移除时,这两个方法将具有完全相同的声明:

    /*

    java.lang.ClassFormatError: Duplicate method name&signature...

    */

    fun sum(ints: List) {

        println("Ints")

    }

    fun sum(strings: List) {

        println("Ints")

    }

我们还可以通过更改生成函数的 JVM 名称来解决此问题。我们可以使用JvmName注解在将代码编译为 JVM 字节码时更改其中一个方法的名称:

    @JvmName("intSum") fun sum(ints: List<Int>) {

        println("Ints")

    }

    fun sum(strings: List<String>) {

        println("Ints")

    }

从 Kotlin 中使用此函数的用法没有改变,但由于我们更改了第一个函数的 JVM 名称,因此我们需要使用新名称从 Java 中使用它:

    // Java

    TestKt.intSum(listOfInts);

有时我们希望在运行时保留类型参数,这就是reified 类型参数非常方便的地方。

具体化类型参数

有些情况下,在运行时访问类型参数会很有用,但由于类型擦除,这是不允许的:

    fun <T> typeCheck(s: Any) {

        if(s is T){ 

        // Error: cannot check for instance of erased type: T

            println("The same types")

        } else {

            println("Different types")

        }

    }

为了能够克服 JVM 的限制,Kotlin 允许我们使用特殊的修饰符来在运行时保留类型参数。我们需要使用 reified 修饰符标记类型参数:

    interface View

    class ProfileView: View

    class HomeView: View

    inline fun <reified T> typeCheck(s: Any) { // 1

        if(s is T){

            println("The same types")

        } else {

        println("Different types")

        }

    }

    // Usage

    typeCheck<ProfileView>(ProfileView()) // Prints: The same types

    typeCheck<HomeView>(ProfileView()) // Prints: Different types

    typeCheck<View>(ProfileView()) // Prints: The same types
  1. 类型参数标记为精炼,函数标记为inline

现在我们可以安全地在运行时访问类型参数类型。具体化类型参数仅适用于内联函数,因为在编译过程中(内联),Kotlin 编译器会替换具体化类型参数的实际类。这样,类型参数就不会被类型擦除移除。

我们还可以对具体化类型使用反射来检索有关类型的更多信息:

    inline fun <reified T> isOpen(): Boolean {

        return T::class.isOpen

    }

在 JVM 字节码级别,具体类型或原始类型的包装类型表示具体类型参数的出现。这就是为什么具体类型参数不受类型擦除影响的原因。

使用具体类型参数允许我们以全新的方式编写方法。在 Java 中启动新的Activity,我们需要这样的代码:

    //Java

    startActivity(Intent(this, ProductActivity::class.java))

在 Kotlin 中,我们可以定义startActivity方法,这将使我们以更简单的方式导航到Activity

    inline fun <reified T : Activity> startActivity(context: Context) {

        context.startActivity(Intent(context, T::class.java))

    }

    // Usage

    startActivity<UserDetailsActivity>(context)

我们定义了startActivity方法,并通过使用类型参数传递了关于我们要启动的ActivityProductActivity)的信息。我们还定义了一个显式的具体类型参数边界,以确保我们只能使用Activity(及其子类)作为类型参数

startActivity 方法

为了正确使用startActivity方法,我们需要一种方法来将参数传递给正在启动的ActivityBundle)。可以更新前面的实现以支持这样的参数:

    startActivity<ProductActivity>("id" to 123, "extended" to true)

在前面的示例中,使用键和值填充参数(由内联to函数定义)。但是,此函数的实现超出了本书的范围。但是,我们可以使用现有的实现。Anko库(github.com/Kotlin/anko)已经实现了具有所有必需功能的startActivity方法。我们只需要导入Appcompat-v7-commons依赖项。

    compile "org.jetbrains.anko:anko-appcompat-v7-commons:$anko_version"

Anko 为ContextFragment类定义了扩展,因此我们可以在任何ActivityFragment中使用此方法,就像在类中定义的任何其他方法一样,而无需在类中定义该方法。我们将在第七章中讨论扩展,扩展函数和属性

请注意,具体类型参数有一个主要限制:我们无法从具体类型参数创建类的实例(不使用反射)。其背后的原因是构造函数总是只与具体实例相关联(它永远不会被继承),因此没有构造函数可以安全地用于所有可能的类型参数。

星投影

由于类型擦除,运行时只能获得不完整的类型信息。例如,泛型类型的类型参数是不可用的:

    val list = listOf(1,2,3)

    println(list.javaClass) // Prints: class java.util.Arrays$ArrayList

这导致了一些问题。我们无法执行任何检查来验证List包含哪些类型的元素:

    /*

    Compile time error: cannot check instance of erased type: 

    List<String>

    */

    if(collection is List<Int>) {

        //...

    }

问题出在运行时执行了一个检查,其中关于类型参数的信息是不可用的。然而,与 Java 相反,Kotlin 不允许我们声明原始类型(未使用类型参数的泛型类型):

    SimpleList<> // Java: ok

    SimpleList<> // Kotlin: error

Kotlin 允许我们使用星投影语法,这基本上是一种表明类型参数信息缺失或不重要的方式:

    if(collection is List<*>) {

        //...

    }

通过使用星投影语法,我们说Box存储特定类型的参数:

    class Box<T>

    val anyBox = Box<Any>()

    val intBox = Box<Int>()

    val stringBox = Box<String>()

    var unknownBox: Box<*>

    unknownBox = anyBox // Ok

    unknownBox = intBox // Ok

    unknownBox = stringBox // Ok

请注意Box<*>Box<Any>之间存在差异。如果我们想定义包含任何项的列表,我们将使用Box<Any>。但是,如果我们想定义包含特定类型项的列表,但是这种类型是未知的(可能是AnyIntString等等。但是我们没有关于这种类型的信息),而Box<Any>表示列表包含Any类型的项。我们将使用Box<*>

    val anyBox: Box<Any> = Box<Int> // Error: Type mismatch

如果泛型类型定义了多个类型参数,我们需要为每个缺失的类型参数使用星(*):

    class Container<T, T2>

    val container: Container<*, *>

星投影在我们想对类型执行操作,但是类型参数信息不重要时也很有帮助:

    fun printSize(list: MutableList<*>) {

        println(list.size)

    }

    //usage

    val stringList = mutableListOf("5", "a", "2", "d")

    val intList = mutableListOf(3, 7)

    printSize(stringList) // prints: 4

    printSize(intList) // prints: 2

在前面的示例中,不需要关于类型参数的信息来确定集合大小。使用星投影语法减少了对变异修饰符的需求,只要我们不使用依赖于类型参数的任何方法。

类型参数命名约定

官方的 Java 类型参数命名约定(docs.oracle.com/javase/tutorial/java/generics/types.html)为参数命名定义了以下准则:

按照惯例,类型参数名称为单个大写字母。这与您已经了解的变量命名约定形成鲜明对比,这是有充分理由的。如果没有这个约定,很难区分类型变量和普通类或接口名称。最常用的类型参数名称是:

  • E: 元素(Java 集合框架广泛使用)

  • K: 键

  • N: 数字

  • T: 类型

  • V: 值

  • S,U,V 等:第 2、第 3、第 4 个类型

Kotlin 标准库中的许多类遵循这种约定。对于常见类(ListMatSet等)或定义简单类型参数的类(Box<T>类)来说,这种方式很好用。然而,对于自定义类和多个类型参数,我们很快意识到单个字母包含的信息量不足,有时很难快速判断类型参数代表的数据类型。对于这个问题有一些解决方案。

我们可以确保泛型得到适当的文档记录,是的,这肯定会有所帮助,但我们仍然无法仅通过查看代码来确定类型参数的含义。文档很重要,但我们应该将文档视为辅助信息源,并努力实现最高可能的代码可读性。

多年来,程序员已经开始转向更有意义的命名惯例。Google Java Style Guidegoogle.github.io/styleguide/javaguide.html#s5.2.8-type-variable-names)简要描述了官方 Java 类型参数命名约定和自定义命名约定的混合。他们提倡两种不同的风格。第一种是使用单个大写字母,可选地后跟单个数字(与 Java 描述的SUV名称相反):

    class Box<T, T2>

第二种风格更具描述性,因为它为类型参数添加了有意义的前缀:

    class Box<RequestT>

不幸的是,类型参数名称没有单一的标准。最常见的解决方案是使用单个大写字母。这些都是简化的例子,但请记住,类通常在多个地方使用泛型,因此适当的命名将提高代码的可读性。

总结

在本章中,我们了解了泛型存在的原因,并讨论了定义泛型类和接口以及声明泛型类型的各种方式。我们知道如何通过使用使用点和声明点变异修饰符来处理子类型关系。我们学会了如何处理类型擦除,以及如何使用具体化的类型参数在运行时保留泛型类型。

在下一章中,我们将讨论 Kotlin 最令人兴奋的功能之一-扩展。这个功能允许我们为现有类添加新的行为。我们将学习如何为任何给定的类实现新的方法和属性,包括 Android 框架和第三方库中的最终类。

第七章:扩展函数和属性

在以前的章节中,大多数概念对 Java 开发人员来说都很熟悉。在本章中,我们介绍了一个在 Java 中完全不知道的功能--扩展。这是 Kotlin 最好的功能之一,许多 Kotlin 开发人员都将其称为自己最喜欢的功能之一。扩展在 Android 开发中有了很大的改进。

本章中,我们将涵盖以下主题:

  • 扩展函数

  • 扩展属性

  • 成员扩展函数

  • 通用扩展函数

  • 集合处理

  • 带接收器的函数类型和带接收器的函数文字

  • Kotlin 通用扩展函数到任何对象

  • Kotlin 领域特定语言

扩展函数

所有较大的 Java 项目都有实用程序类,例如StringUtilsListUtilsAndroidUtils等。这是如此流行,因为 util 函数捕捉常见模式,并允许以更简单的方式进行测试和使用。问题在于 Java 对这种函数的创建和使用支持非常差,因为它们必须实现为某个类的静态函数。让我们通过一个例子讨论这个问题。每个 Java Android 开发人员都很熟悉用于显示Toast的以下代码:

    Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); 

在 Android 项目中通常用于显示错误或短消息,并且通常出现在大多数 Android 教程的开头。由于它使用类似构建器的静态函数,实现此功能的代码是冗长的。也许每个 Java Android 开发人员至少有一次忘记在返回的对象上调用show方法,这使他检查所有周围的条件以找出为什么这不起作用。所有这些都使得这个简单的功能成为打包为 util 函数的完美候选者。但实际上很少以这种方式使用。为什么?要理解这一点,让我们首先看看它在 Java 中是如何实现的:

public class AndroidUtils { 
    public static void toast(Context context, String text) { 
        Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); 
    } 
} 

// Usage 
AndroidUtils.toast(context, "Some toast"); 

当程序员想要使用以下函数时,他们需要记住有这样的函数,在哪个类中定位,以及它的名称是什么。因此,它的使用并不比以前简单。在不改变 Android SDK 实现的情况下,无法将其实现为ContextActivity的超类)的方法,但在 Kotlin 中,可以创建一个扩展函数,其行为类似于在类内部定义的实际方法。以下是我们如何将toast实现为Context的扩展:

    fun Context.toast(text: String) { // 1 
        Toast.makeText(this, text, LENGTH_LONG).show() //2 
    } 

    // Usage 
    context.toast("Some toast")  
  1. Context 不在参数列表中,而是在函数名之前。这是我们定义我们要扩展的类型的方式。

  2. 在函数体内,我们可以使用 this 关键字引用调用扩展函数的对象。

扩展函数和标准函数之间在一般结构上的唯一区别是在函数名之前指定了接收器类型。在函数体内部,我们可以使用this关键字访问接收器对象(调用扩展的对象),或直接调用其函数或属性。有了这样的定义,toast函数就像在Context中定义的方法一样:

    context.toast("Some toast") 

    Alternatively: 
    class MainActivity :Activity() { 

        override fun onCreate(savedInstanceState: Bundle?){ 
            super.onCreate(savedInstanceState) 
            toast("Some text") 
        } 
    } 

这使得使用toast函数比实现整个显示 toast 的代码更容易。我们还可以从 IDE 中得到建议,在Context(如在Activity内部)或Context的实例中调用此函数时,我们可以调用此函数:

在前面的例子中,Contexttoast 函数的接收器类型,this 实例是对接收器对象的引用。可以显式访问接收器对象的所有函数和属性,因此我们可以采用以下定义:

    fun Collection<Int>.dropPercent(percent: Double) 
        = this.drop(floor(this.size * percent) 

然后我们可以用以下内容替换它:

    fun Collection<Int>.dropPercent(percent: Double) 
        = drop(floor(size * percent)) 

扩展函数有多种有用的用例。类似的扩展函数可以为ViewListString和 Android 框架或第三方库中定义的其他类以及开发人员定义的自定义类定义。扩展函数可以添加到任何可访问的类型,甚至可以添加到Any对象。以下是一个可以在每个对象上调用的扩展函数:

fun Any?.logError(error: Throwable, message: String = "error") { 
    Log.e(this?.javaClass?.simpleName ?: "null", message, error) 
} 

以下是一些调用示例:

    user.logError(e, "NameError") // Logs: User: NameError ... 
    "String".logError(e) // String: error ... 
    logError(e) // 1, MainActivity: error ... 
  1. 假设我们在MainActivity中调用这个函数。

我们可以简单地向任何我们想要的类添加任何方法。这对于 Android 开发是一个很大的改进。有了它,我们有一种方法可以向类型添加丢失的方法或属性。

扩展函数的内部机制

虽然 Kotlin 扩展函数可能看起来像魔术,但在幕后它们实际上非常简单。顶级扩展函数被编译为具有接收对象的静态函数的第一个参数。让我们看看已经介绍的toast函数:

    // ContextExt.kt 

    fun Context.toast(text: String) { 
        Toast.makeText(this, text, LENGTH_LONG).show() 
    } 

这个函数在编译和反编译为 Java 后,会看起来类似于以下函数:

//Java 
public class ContextExtKt { 
    public static void toast(Context receiver, String text) { 
        Toast.makeText(receiver, text, Toast.LENGTH_SHORT).show(); 
    } 
}

Kotlin 顶级扩展函数被编译为具有接收对象的静态函数的第一个参数。这就是为什么我们仍然可以从 Java 中使用扩展函数:

    // Java 
    ContextExtKt.toast(context, "Some toast") 

此外,这意味着从 JVM 字节码的角度来看,该方法并没有真正添加,但在编译期间,所有扩展函数的用法都被编译为静态函数调用。虽然扩展函数只是函数,但函数修饰符可以应用于它们,就像它们也可以应用于任何其他函数一样。例如,扩展函数可以标记为inline

inline fun Context.isPermissionGranted (permission: String): Boolean = ContextCompat.checkSelfPermission (this, permission) ==  PackageManager.PERMISSION_GRANTED 

与其他inline函数一样,函数调用将在应用程序编译期间替换为实际的主体。我们可以使用扩展函数实际上可以做任何我们可以做的事情。它们可以是单个表达式,具有默认参数,可以通过命名参数使用等等。但这种实现还有其他一些不太直观的后果。在接下来的章节中,我们将对它们进行描述。

没有方法重写

当存在具有相同名称和参数的成员函数和扩展函数时,成员函数总是优先。这是一个例子:

    class A { 
        fun foo() { 
            println("foo from A") 
        } 
    } 

    fun A.foo() { 
        println("foo from Extension") 
    } 

    A().foo() // Prints: foo from A 

这总是成立的。即使是来自超类的方法也会受到扩展函数的影响:

    open class A { 
        fun foo() { 
            println("foo from A") 
        } 
    } 

    class B: A() 

    fun B.foo() { 
        println("foo from Extension") 
    } 

    A().foo() // foo from A 

关键是扩展函数不允许修改真实对象的行为。我们只能添加额外的功能。这使我们得到了保障,因为我们知道没有人会改变我们正在使用的对象的行为,这可能导致难以追踪的错误。

访问接收器元素

扩展函数被编译为具有接收对象的静态函数的第一个参数,因此我们没有额外的访问权限。privateprotected元素是不可访问的,具有 Java default,Java package或 Kotlin internal修饰符的元素与我们对标准对象进行操作时一样被访问。

由于这样,这些元素得到了应有的保护。请记住,扩展函数虽然强大而有用,但只是一种语法糖,其中并没有魔法。

扩展是静态解析的

扩展函数只是具有接收器作为第一个参数的函数,因此它们的调用是由调用函数的类型在编译时解析的。例如,当超类和子类都有扩展函数时,那么在调用期间选择的扩展函数取决于我们正在操作的属性的类型。这是一个例子:

    abstract class A 
    class B: A() 

    fun A.foo() { println("foo(A)") } 
    fun B.foo() { println("foo(B)") } 

    val b = B() 
    b.foo() // prints: foo(B) 
    (b as A).foo() // 1, prints: foo(A) 
    val a: A = b 
    a.foo() // 1, prints: foo(A) 
  1. 在这里,我们期望foo(B),而实际上对象的类型是B,但是由于扩展是静态解析的,它使用了A的扩展函数,因为变量的类型是A,在编译期间没有关于对象是什么的信息。

这个事实有时是有问题的,因为当我们为我们最常转换的类型定义扩展函数时,我们不应该为它的子类实现扩展函数。

这是一个重要的限制,应该牢记在心,特别是在公共库实现期间,因为这种方式,一些扩展函数可能会阻止其他函数并导致意外行为。

伴生对象扩展

如果一个类定义了一个伴生对象,那么您也可以为这个伴生对象定义扩展函数(和属性)。要区分对类的扩展和对伴生对象的扩展,需要在扩展类型和函数名称之间添加.Companion

    class A { 
        companion object {} 
    } 
    fun A.Companion.foo() { print(2) } 

当它被定义时,foo方法可以像在A伴生对象内定义的方法一样使用:

    A.foo() 

请注意,我们使用类类型而不是类实例来调用此扩展。要允许为伴生对象创建扩展函数,需要在类内明确定义一个伴生对象。即使是一个空的。没有它,就不可能定义扩展函数:

使用扩展函数进行操作符重载

操作符重载是 Kotlin 的一个重要特性,但通常我们需要使用 Java 库和那里未定义的操作符。例如,在 RxJava 中,我们使用CompositeDisposable函数来管理订阅。这个集合使用add方法来添加新元素。这是一个添加到CompositeDisposable的示例订阅

    val subscriptions = CompositeDisposable() 

    subscriptions.add(repository 
       .getAllCharacters(qualifiedSearchQuery) 
       .subscribeOn(Schedulers.io()) 
       .observeOn(AndroidSchedulers.mainThread()) 
       .subscribe(this::charactersLoaded, view::showError)) 

向可变集合添加新元素的标准 Kotlin 方式是使用plusAssign运算符(+=)。这不仅更通用,而且更清晰,而且我们可以省略括号:

    val list = mutableListOf(1,2,3) 
    list.add(1) 
    list += 1 

要在我们的示例中应用它,我们可以添加以下扩展:

operator fun CompositeDisposable.plusAssign(disposable: Disposable) 

{ 
    add(disposable)  
} 

现在我们可以在CompositeDisposable上使用plusAssign方法:

    subscriptions += repository 
       .getAllCharacters(qualifiedSearchQuery) 
       .subscribeOn(Schedulers.io()) 
       .observeOn(AndroidSchedulers.mainThread()) 
       .subscribe(this::charactersLoaded, view::showError)

顶级扩展函数应该在哪里使用?

扩展函数在我们觉得其他程序员定义的类缺少某些方法时最常用。例如,如果我们认为View应该包含showhide方法,使用起来比可见性字段设置更容易,那么我们可以自己实现它:

    fun View.show() { visibility = View.VISIBLE } 
    fun View.hide() { visibility = View.GONE } 

无需记住保存 util 函数的类的名称。在 IDE 中,我们只需在对象后面加一个点,就可以搜索与该对象一起提供的所有方法,包括来自项目和库的扩展函数。调用看起来不错,就像原始对象成员一样。这就是扩展函数的美,但也是一个危险。现在已经有大量的 Kotlin 库,它们只是一堆扩展函数。当我们使用大量扩展函数时,我们的 Android 代码可能不像正常的 Android 代码。这既有利有弊。以下是利:

  • 代码更短,更易读

  • 代码呈现更多逻辑而不是 Android 样板

  • 扩展函数通常在多个地方进行测试,或者至少在多个地方使用,因此更容易找出它们是否正常工作

  • 当我们使用扩展函数时,我们很少会犯一个愚蠢的错误,导致数小时的代码调试

为了说明最后两点,我们将回到toast函数。在编写以下内容时很难出错:

    toast("Some text") 

虽然在以下情况下更容易出错:

    Toast.makeText(this, "Some text",Toast.LENGTH_LONG).show() 

在项目中强大的扩展使用的最大问题是,实际上我们正在制定自己的 API。我们正在命名和实现函数,并决定应该有哪些参数。当一些开发人员加入团队时,他需要学习我们创建的整个 API。Android API 有很多缺点,但它的优势在于它是通用的,并且所有 Android 开发人员都知道它。

这是否意味着我们应该放弃扩展?绝对不是!这是一个很好的功能,可以帮助我们使代码简洁清晰。关键是我们应该以聪明的方式使用它们:

  • 避免执行相同操作的多个扩展。

  • 短小简单的功能通常不需要成为扩展。

  • 在项目周围保持一种编码风格。与您的团队交流并指定一些标准。

  • 在使用具有扩展功能的公共库时要小心。将它们保留为无法更改的代码,并将您的扩展与它们匹配以保持 API 清晰。

扩展属性

在本节中,我们将首先了解什么是扩展属性,然后我们将继续学习这些属性可以在哪里使用。正如我们已经知道的,Kotlin 中的属性是由它们的访问器(getter 和 setter)定义的:

    class User(val name: String, val surname: String) { 
        val fullName: String 
        get() = "$name $surname" 
    } 

我们还可以定义扩展属性。唯一的限制是该属性不能有后备字段。原因是扩展不能存储状态,因此没有好地方来存储此字段。以下是TextView的扩展属性定义示例:

    val TextView.trimmedText: String 
    get() = text.toString().trim() 

    // Usage 
    textView.trimmedText 

与扩展函数一样,上述实现将被编译为具有第一个参数的接收器的访问器函数。以下是 Java 中的简化结果:

    public class AndroidUtilsKt { 
        String getTrimmedText(TextView receiver) { 
            return receiver.getText().toString().trim(); 
        } 
    } 

如果它是一个读写属性,那么 setter 和 getter 都将被实现。请记住,只有不需要 Java 字段的属性才允许被定义为扩展属性。例如,这是非法的:

扩展属性应该在哪里使用?

扩展属性通常可以与扩展函数互换使用。它们通常都用作顶级工具。当我们希望对象具有一些未经原生开发的属性时,就会使用扩展属性。决定我们应该使用扩展函数还是扩展属性的决定几乎与我们在类内部使用不带后备字段的函数或属性的决定相同。只是提醒一下,根据惯例,当底层算法满足以下条件时,应优先选择属性而不是函数:

  • 不会抛出错误

  • 具有 O(1)复杂度

  • 计算成本低(或在第一次运行时缓存)

  • 在调用中返回相同的结果

让我们来看一个简单的问题。我们经常需要在 Android 中获取一些服务,但用于获取它们的代码很复杂:

    PreferenceManager.getDefaultSharedPreferences(this) 
    getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater 
    getSystemService(Context.ALARM_SERVICE) as AlarmManager 

要使用诸如AlarmManagerLayoutInflater之类的服务,程序员必须记住每个服务的以下内容:

  • 提供它的函数的名称(例如getSystemService)和包含它的类(例如Context

  • 指定此服务的字段名称(例如Context.ALARM_SERVICE

  • 服务应该转换为的类的名称(例如AlarmManager

这很复杂,这是我们可以通过扩展属性优化使用的完美场所。我们可以这样定义扩展属性:

    val Context.preferences: SharedPreferences 
        get() = PreferenceManager 
            .getDefaultSharedPreferences(this) 

    val Context.inflater: LayoutInflater 
        get() = getSystemService(Context.LAYOUT_INFLATER_SERVICE) 
            as LayoutInflater 

    val Context.alarmManager: AlarmManager 
        get() = getSystemService(Context.ALARM_SERVICE) 
            as AlarmManager 

从现在开始,我们可以像Context的属性一样使用preferencesinflateralarmManager

context.preferences.contains("Some Key") 
context.inflater.inflate(R.layout.activity_main, root) 
context.alarmManager.setRepeating(ELAPSED_REALTIME, triggerAt, 

   interval, pendingIntent) 

这些都是良好的只读扩展函数使用的完美例子。让我们专注于inflater扩展属性。它有助于获取通常需要的元素,但是没有扩展很难获取。这是有帮助的,因为程序员只需要记住他们需要的是一个 inflater,他们需要Context来拥有它,而不需要记住提供系统服务的方法的名称(getSystemService),获取inflater属性的键的名称(ALARM_SERVICE),它位于哪里(在Context中),以及这个服务应该被转换为什么(AlarmManager)。换句话说,这个扩展节省了很多工作和程序员的记忆。此外,根据指南,属性 getter 的执行时间很短,复杂度为O(1),它不会抛出任何错误,并且总是返回相同的inflater(实际上,它可能是一个不同的实例,但从程序员的角度来看,它的使用方式总是相同的,这才是重要的)。

我们已经看到了只读扩展属性,但我们还没有看到读写扩展属性。这是一个很好的例子,是对扩展函数部分中我们看到的hideshow函数的替代品:

    var View.visible: Boolean 
    get() = visibility == View.VISIBLE 
    set(value) { 
        visibility = if (value) View.VISIBLE else View.GONE 
    } 

我们可以使用这个属性来改变视图元素的可见性:

    button.visible = true // the same as show() 
    button.visible = false // the same as hide() 

    Also, we can check view element visibility: 

    if(button.visible) { /* ... */ } 

一旦我们定义了它,我们就可以将其视为真正的View属性。重要的是,我们设置的内容与我们获取的内容保持一致。因此,假设没有其他线程改变元素的可见性,我们可以设置一些属性值:

    view.visible = true 

然后 getter 将始终提供相同的值:

    println(view.visible) // Prints: true 

最后,getter 和 setter 内部没有其他逻辑,只是特定属性的更改。因此,我们之前提出的其他约定也得到了满足。

成员扩展函数和属性

我们已经看到了顶层扩展函数和属性,但也可以在类或对象内部定义它们。在那里定义的扩展称为成员扩展,它们通常用于不同于顶层扩展的各种问题。

让我们从成员扩展被使用的最简单的用例开始。假设我们需要删除String列表的每第三个元素。这是一个允许我们删除每个 i^(th)元素的扩展函数:

    fun List<String>.dropOneEvery(i: Int) = 
        filterIndexed { index, _ -> index % i == (i - 1) } 

该函数的问题在于,它不应该被提取为一个 util 扩展,因为:

  • 它没有准备好用于不同类型的列表(比如User列表或Int列表)

  • 这是一个很少有用的函数,所以可能不会在项目的其他地方使用

这就是为什么我们希望将其保持为私有,并且将其作为成员扩展函数放在我们使用它的类内部的好主意:

    class UsersItemAdapter : ItemAdapter() { 
        lateinit var usersNames: List<String> 

        fun processList() { 
        usersNames = getUsersList() 
            .map { it.name } 
            .dropOneEvery(3) 
        } 

        fun List<String>.dropOneEvery(i: Int) = 
            filterIndexed { index, _ -> index % i == (i - 1) } 

        // ... 
    } 

这是我们使用成员扩展函数的第一个原因,是为了保护函数的可访问性。在这种情况下,可以通过在同一文件中的顶层定义一个带有私有修饰符的函数来实现。但是成员扩展函数的行为与顶层函数不同。在前面的代码中使用的函数是公共的,但它只能在List<String>上调用,并且只能在UsersItemAdapter中调用。因此,它只能在UsersItemAdapter类及其子类内部或在UsersItemAdapter的扩展函数内部使用:

    fun UsersItemAdapter.updateUserList(newUsers: List<User>) { 
        usersNames = newUsers 
           .map { it.name } 
           .dropOneEvery(3) 
    } 

请注意,要使用成员扩展函数,我们需要实现它的对象和将调用这些扩展函数的对象。这是因为我们可以使用这两个对象的元素。这是关于成员扩展的重要信息:它们可以在没有限定符的情况下使用接收器类型和成员类型的元素。让我们看看它可能如何使用。这是另一个例子,类似于前一个例子,但它使用了私有属性category

    class UsersItemAdapter( 
        private val category: Category 
    ) : ItemAdapter() { 

       lateinit var usersNames: List<String> 

       fun processList() { 
            usersNames = getUsersList() 
                .fromSameCategory() 
                .map { it.name } 
       } 

       fun List<User>.fromSameCategory() = 
           filter { u -> u.category.id == category.id } 

       private fun getUsersList() = emptyList<User>() 
    } 

在成员扩展函数fromSameCategory内部,我们正在操作一个扩展接收器(List<User>),但我们也在使用UsersItemAdapter中的category属性。我们在这里看到,以这种方式定义的函数需要是一个方法,并且可以类似于其他方法一样使用。与标准方法相比的优势在于我们可以在List上调用函数,因此我们可以保持清晰的流处理,而不是使用非扩展方法:

    // fromSameCategory defined as standard method 
    usersNames = fromSameCategory(newUsers) 
         .dropLast(3) 

    // fromSameCategory defined as member extension function 
    usersNames = newUsers

         .fromSameCategory() 
         .dropLast(3) 

另一个常见的用法是成员扩展函数或属性可以像普通方法一样使用,但我们正在利用成员函数内部可以使用接收器属性和方法而不命名它们的事实,这样我们可以有更短的语法,并且我们实际上是在接收器上调用它们,而不是使用相同类型作为参数调用它们。例如,我们可以采用以下方法:

    private fun setUpRecyclerView(recyclerView: RecyclerView) { 
        recyclerView.layoutManager  
            = LinearLayoutManager(recyclerView.context) 
        recyclerView.adapter 
            = MessagesAdapter(mutableListOf()) 
    } 

    // Usage 
    setUpRecyclerView(recyclerView) 

然后我们可以用以下成员扩展函数替换它:

    private fun RecyclerView.setUp() { 
        layoutManager = LinearLayoutManager(context) 
        adapter = MessagesAdapter(mutableListOf()) 
    } 

    // Usage 
    recyclerView.setUp() 

使用成员扩展函数,我们可以实现更简单的调用和更简单的函数体。这次尝试的最大问题是不清楚我们使用的哪些函数是RecyclerView的成员,哪些是ActivityRecyclerView扩展的成员。这个问题将在接下来的页面中提出。

接收器的类型

当我们有一个成员扩展函数时,管理我们正在调用哪些元素变得更加复杂。在成员扩展内部,我们隐式访问以下内容:

  • 来自此类及其超类的成员函数和属性。

  • 接收器类型的函数和属性,包括来自接收器类型及其超类型的函数和属性

  • 顶级函数和属性

因此,在setUp扩展函数内部,我们可以同时使用成员和接收器的方法和属性:

    class MainActivity: Activity() { 

       override fun onCreate(savedInstanceState: Bundle?) { 
           super.onCreate(savedInstanceState) 
           setContentView(R.layout.main_activity) 
           val buttonView = findViewById(R.id.button_view) as Button 
           buttonView.setUp() 
       } 

       private fun Button.setUp() { 
           setText("Click me!") // 1, 2 
           setOnClickListener { showText("Hello") } // 2 
       } 

       private fun showText(text: String) { 
           toast(text) 
       } 
    } 
  1. setTextButton类的方法。

  2. 我们可以交替使用Button类和MainActivity类的成员。

这可能有点棘手--也许大多数人不会注意到是否存在错误,并且setText调用将与showText调用交换。

虽然我们可以在成员扩展内部使用来自不同接收器的元素,为了区分它们,所有类型的接收器都被命名。首先,所有可以被this关键字使用的对象都被称为隐式接收器。它们的成员可以在没有限定符的情况下访问。在setUp函数内部,有两个隐式接收器:

  • 扩展接收器:扩展定义的类的实例(Button

  • 分发接收器:扩展声明的类的实例(MainActivity

请注意,虽然扩展接收器和分发接收器的成员都是同一个函数体中的隐式接收器,但可能存在一种情况,我们在两者中都使用具有相同签名的成员。例如,如果我们将上一个类更改为在textView中显示文本而不是在toast函数中显示它,并将方法名称更改为setText,那么我们将使用具有相同签名的分发和扩展接收器的方法(一个在Button类中定义,另一个在MainActivity类中定义):

    class MainActivity: Activity() { 

        override fun onCreate(savedInstanceState: Bundle?) { 
            super.onCreate(savedInstanceState) 
            setContentView(R.layout.main_activity) 
            val buttonView = findViewById(R.id.button_view) as Button 
            buttonView.setUp() 
        } 

        private fun Button.setUp() { 
            setText("Click me!") 
            setOnClickListener { setText("Hello") } // 1 
        } 

        private fun setText(text: String) { 
            textView.setText(text) 
        } 
    } 
  1. setText既是分发接收器的方法,也是扩展接收器的方法。哪一个会被调用?

因此,setText函数将从扩展接收器中调用,并且按钮点击将更改所点击按钮的文本!这是因为扩展接收器始终优先于分发接收器。但是,在这种情况下,仍然可以使用分发接收器,方法是使用限定的 this 语法(带有标签的this关键字,即区分我们要引用的接收器):

    private fun Button.setUp() { 
        setText("Click me!") 
        setOnClickListener {  
            this@MainActivity.setText("Hello")  
        } 
    } 

这样,我们可以解决区分分发接收器和扩展接收器的问题。

成员扩展函数和属性在底层

成员扩展函数和属性与顶级扩展函数和属性编译方式相同,唯一的区别是它们在类内部,并且它们不是静态的。这是一个简单的扩展函数的例子:

    class A { 
        fun boo() {} 

        fun Int.foo() { 
            boo() 
         } 
    } 

这是它编译后的样子(经过简化):

    public final class A { 
        public final void boo() { 
            ...

        } 

        public final void foo(int $receiver) { 
            this.boo(); 
        } 
    } 

请注意,虽然它们只是带有接收者作为第一个参数的方法,但我们可以像其他函数一样使用它们。访问修饰符的工作方式相同,如果我们将成员扩展函数定义为 open,那么我们可以在其子类中重写它。

通用扩展函数

当我们编写实用函数时,通常希望它们是通用的。最常见的例子是对集合(ListMapSet)的扩展。这里有一个对List的扩展属性的例子:

    val <T> List<T>.lastIndex: Int 
        get() = size - 1 

前面的例子定义了一个通用类型的扩展属性。这种扩展用于许多不同的问题。例如,启动另一个Activity是一个重复的任务,通常需要在项目中的多个地方实现。Android IDE 提供的用于启动Activity的方法并不简单。这是用于启动名为SettingsActivity的新 Activity 的代码:

    startActivity(Intent (this, SettingsActivity::class.java)) 

请注意,这种简单而重复的任务需要大量不太清晰的代码。但是我们可以定义扩展函数,使用reified类型的通用内联扩展函数来更简单地创建Intent和启动没有参数的Activity

    inline fun <reified T : Any> Context.getIntent() 
       = Intent(this, T::class.java) 

    inline fun <reified T : Any> Context.startActivity() 
       = startActivity(getIntent<T>()) 

现在我们可以通过以下简单地启动Activity

    startActivity<SettingsActivity>() 

或者我们可以这样创建intent

    val intent = getIntent<SettingsActivity>() 

这样,我们可以以较低的成本使这个常见的任务更容易。此外,像Ankogithub.com/Kotlin/anko)这样的库提供了扩展函数,提供了一种简单的方式来启动带有额外参数或标志的Activity,就像这个例子中一样:

    startActivity<SettingsActivity>(userKey to user) 

本书不涵盖库的内部实现,但我们可以通过将 Anko 库依赖添加到我们的项目中来简单地使用这个扩展。这个例子的重点是几乎所有重复的代码都可以用扩展来替换为更简单的代码。还有其他启动Activity的替代方式,比如基于参数注入的ActivityStarter库(github.com/MarcinMoskala/ActivityStarter),它对 Kotlin 有很好的支持。它允许经典的参数注入:

    class StudentDataActivity : BaseActivity() {

        lateinit @Arg var student: Student

        @Arg(optional = true) var lesson: Lesson = Lesson.default()

    }

或者,作为替代方案,它允许在 Kotlin 属性委托中进行延迟注入(这在第八章,委托中有描述):

  class StudentDataActivity : BaseActivity() {

      @get:Arg val student: Student by argExtra()

      @get:Arg(optional = true) 

      var lesson: Lesson by argExtra(Lesson.default())

  }

带有这些参数的Activity可以使用生成的静态函数启动:

    StudentDataActivityStarter.start(context, student, lesson)

    StudentDataActivityStarter.start(context, student) 

让我们看另一个例子。在 Android 中,我们经常需要以 JSON 格式存储对象。例如,当我们需要将它们发送到 API 或将它们存储在文件中时。用于将对象序列化和反序列化为 JSON 的最流行的库是 Gson。让我们看一下使用 Gson 库的标准方式:

    val user = User("Marcin", "Moskala") 
    val json: String = globalGson.toJson(user) 
    val userFromJson = globalGson.fromJson(json, User::class.java) 

我们可以通过带有inline修饰符的扩展函数在 Kotlin 中改进它。这里有一个使用 GSON 将对象打包和解包为 JSON 格式的扩展函数的例子:

    inline fun Any.toJson() = globalGson.toJson(this)!! 

    inline fun <reified T : Any> String.fromJson() 
      = globalGson.fromJson(this, T::class.java) 

    // Usage 
    val user = User("Marcin", "Moskala") 
    val json: String = user.toJson() 
    val userFromJson: User = json.fromJson<User>() 

globalGson实例是Gson的全局实例。这是常见的做法,因为我们经常定义一些序列化程序和反序列化程序,这是定义它们和构建Gson实例的更简单和更有效的方式。

示例展示了通用扩展函数给开发人员提供了哪些可能性。它们就像代码提取的下一个级别:

  • 它们是顶层的,但也可以在对象上调用,所以很容易管理

  • 它们是通用的,因此是通用的,可以应用于任何东西

  • 当内联时,它们允许我们定义reified类型参数

这就是为什么泛型扩展函数在 Kotlin 中经常使用。此外,标准库提供了许多泛型扩展。在下一节中,我们将看到一些集合扩展函数。这部分很重要,不仅因为它提供了关于泛型扩展函数的使用知识,而且因为它最终描述了 Kotlin 中列表处理的工作原理以及如何使用它。

集合处理

集合处理是编程中最常见的任务之一。这就是为什么开发人员学习的第一件事之一是如何迭代集合以对元素进行操作。要求年轻的开发人员从列表中打印所有用户,他们很可能会使用for循环:

    for (user in users) { 
        println(user) 
    } 

如果我们要求他们只显示在学校通过的用户,那么他们很可能会在这个循环中添加一个if条件:

    for (user in users) { 
        if ( user.passing ) {   
            println(user) 
        } 
    } 

这仍然是正确的实现,但当任务变得更加复杂时,真正的问题就开始了。如果他们被要求打印通过的三名最优秀的学生呢?在循环中实现这一点非常复杂,而在 Kotlin 流处理中实现它却很简单。让我们在示例中看看。这是学生的示例列表:

    data class Student( 
        val name: String,  
        val grade: Double,  
        val passing: Boolean 
    ) 

    val students = listOf( 
        Student("John", 4.2, true), 
        Student("Bill", 3.5, true), 
        Student("John", 3.2, false), 
        Student("Aron", 4.3, true), 
        Student("Jimmy", 3.1, true) 
    ) 

让我们使用从 Java 中已知的命令式方法(使用循环和排序方法)来过滤学生:

    val filteredList = ArrayList<Student>() 
    for (student in students) { 
        if(student.passing) filteredList += student 
    } 

    Collections.sort(filteredList) { p1, p2 -> 
        if(p1.grade > p2.grade) -1 else 1 
    } 

    for (i in 0..2) { 
        val student = filteredList[i] 
        println(student) 
    } 

    // Prints: 
    // Student(name=Aron, grade=4.3, passing=true) 
    // Student(name=John, grade=4.2, passing=true) 
    // Student(name=Bill, grade=3.5, passing=true) 

我们可以使用 Kotlin 流处理以更简单的方式实现相同的结果:

    students.filter { it.passing } // 1 
       .sortedByDescending { it.grade } // 2 
       .take(3) // 3 
       .forEach(::println) // 4 
  1. 只取通过的学生。

  2. 根据他们的成绩对学生进行排序(降序以使成绩更好的学生处于更高的位置)。

  3. 只取前三个。

  4. 逐个打印它们。

关键在于每个流处理函数,例如前面示例中的sortedByDescendingtakeforEach,都提取了一个小功能,其威力来自它们的组合。结果比使用经典循环更简单和更可读。

流处理实际上是一种非常常见的语言特性。它在 C#,JavaScript,Scala 和许多其他语言中都有,包括自 Java 8 版本以来。流行的响应式编程库,如 RxJava,也大量利用这个概念来处理数据。在本节中,我们将深入研究 Kotlin 集合处理。

Kotlin 集合类型层次结构

Kotlin 类型层次结构设计得非常好。标准集合实际上是来自本地语言(如 Java)的集合,这些集合隐藏在接口后面。它们的创建是通过标准的顶层函数(listOfsetOfmutableListOf等)进行的,因此它们可以在通用模块(编译为多个平台的模块)中创建和使用。此外,Kotlin 接口可以像它们在 Java 中的等效接口一样起作用(如ListSet等),这使得 Kotlin 集合高效且与外部库高度兼容。同时,Kotlin 集合接口层次结构可以在通用模块中使用。这个层次结构很简单,了解它是有利可图的:

Kotlin 集合接口层次结构

最通用的接口是Iterable。它表示可以迭代的元素序列。任何实现iterable的对象都可以在for循环中使用:

    for (i in iterable) { /* ... */ } 

许多不同类型都实现了可迭代接口:所有集合,进展(1..10'a'..'z'),甚至String。它们都允许我们迭代它们的元素:

    for (char in "Text") { print("($char)") } // Prints: (T)(e)(x)(t) 

Collection接口表示元素的集合并扩展Iterable。它添加了size属性和containscontainsAllisEmpty方法。

Collection继承的两个主要接口是ListSet。它们之间的区别在于Set是无序的,不包含重复元素(根据equals方法)。ListSet接口都不包含任何允许我们改变对象状态的方法。这就是为什么默认情况下,Kotlin 集合被视为不可变的。当我们有一个List的实例时,它在 Android 中很可能是ArrayListArrayList是一个可变集合,但是当它隐藏在List接口后,它实际上的行为就像是不可变的,因为它不公开任何允许我们应用更改的方法(除非进行向下转型)。

在 Java 中,集合是可变的,但 Kotlin 集合接口默认只提供不可变行为(例如,没有改变集合状态的方法,比如addremoveAt):

    val list = listOf('a', 'b', 'c') 
    println(list[0]) // Prints: a 
    println(list.size) // Prints: 3 
    list.add('d') // Error 
    list.removeAt(0) // Error 

所有不可变接口(CollectionList等)都有它们的可变等价物(MutableCollectionMutableList等),它们继承自相应的不可变接口。可变意味着实际对象可以被修改。这些接口代表了标准库中的可变集合:

  • MutableIterable允许在应用更改的情况下进行迭代

  • MutableCollection确保添加和删除元素的方法

  • MutableListMutableSetListSet的可变等价物

现在我们可以修复之前的例子,并使用addremove方法更改集合:

    val list = mutableListOf('a', 'b', 'c') 
    println(list[0]) // Prints: a 
    println(list.size) // Prints: 3 
    list.add('d') 
    println(list) // Prints: [a, b, c, d] 
    list.removeAt(0) 
    println(list) // Prints: [b, c, d] 

不可变和可变接口都只提供了一些方法,但是 Kotlin 标准库为它们提供了许多有用的扩展:

这使得处理集合比在 Java 中更容易。

Kotlin 使用扩展来实现集合处理方法。这种方法有很多优势;例如,如果我们想要实现一个自定义集合(比如List),我们只需要实现一个只包含几个方法的iterable接口。我们仍然可以使用为iterable接口提供的所有扩展。

另一个原因是这些函数作为接口的扩展函数时可以灵活使用。例如,大多数这些集合处理函数实际上是Iterable的扩展,而Iterable被许多更多的类型实现,例如StringRange。因此,也可以在IntRange上使用所有扩展函数。这是一个例子:

    (1..5).map { it * 2 }.forEach(::print) // Prints: 246810

这使得所有这些扩展真正通用。集合流处理方法作为扩展函数实现也有一个缺点。虽然扩展是静态解析的,但是为特定类型覆盖扩展函数是不正确的,因为当它在接口后面时,其行为将与直接访问时不同。

让我们分析一些用于集合处理的扩展函数。

map、filter、flatMap 函数

我们已经简要介绍了mapfilterflatMap,因为它们是最基本的流处理函数。map函数返回根据参数中的函数更改的元素的列表:

    val list = listOf(1,2,3).map { it * 2 }

    println(list) // Prints: [2, 4, 6]

filter函数只允许与提供的谓词匹配的元素:

    val list = listOf(1,2,3,4,5).map { it > 2 } 

    println(list) // Prints: [3, 4, 5] 

flatMap函数返回由原始集合的每个元素调用的变换函数产生的所有元素的单个列表:

    val list = listOf(10, 20).flatMap { listOf(it, it+1, it + 2) } 
    println(list) // Prints: [10, 11, 12, 20, 21, 22] 

它通常用于展平集合的列表:

    shops.flatMap { it.products } 
    schools.flatMap { it.students } 

让我们看一下这些扩展函数的简化实现:

inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> { //1 
    val destination = ArrayList<R>() 
    for (item in this) destination.add(transform(item)) // 2 
    return destination 
} 

inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { // 1 
    val destination = ArrayList<T>() 
    for (item in this) if(predicate(item)) destination.add(item) // 2 
    return destination 
} 

inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Collection<R>): List<R> { 

// 1 
    val destination = ArrayList<R>() 
    for (item in this) destination.addAll(transform(item)) // 2 
    return destination 
} 
  1. 所有这些函数都是内联的。

  2. 所有这些函数内部都使用 for 循环,并返回一个包含适当元素的新列表。

大多数 Kotlin 标准库扩展函数类型都是inline的,因为它使 lambda 表达式的使用更有效率。因此,整个集合流处理实际上在运行时大部分编译为嵌套循环。例如,这是一个简单的处理:

    students.filter { it.passing } 
       .map { "${it.name} ${it.surname}" } 

编译和反编译为 Java 后,它看起来像下面这样(清理过的):

    Collection destination1 = new ArrayList(); 
    Iterator it = students.iterator(); 
    while(it.hasNext()) { 
        Student student = (Student) it.next(); 
        if(student.getPassing()) { 
            destination1.add(student); 
        } 
    }  
    Collection destination2 = new ArrayList(destination1.size()); 
    it = destination2.iterator(); 
    while(it.hasNext()) { 
        Student student = (Student) it.next(); 
        String var = student.getName() + " " + student.getSurname(); 
        destination2.add(var); 
    } 

forEachonEach函数

forEach函数已经在关于函数的章节中讨论过。它是for循环的一种替代方法,因此它对列表中的每个元素执行一个操作:

    listOf("A", "B", "C").forEach { print(it) } // prints: ABC 

自 Kotlin 1.1 以来,还有一个类似的函数onEach,它也对每个元素执行一个操作。它返回一个扩展接收器(这个列表),所以我们可以在流处理的中间对每个元素执行一个操作。常见的用例是日志记录。以下是一个例子:


    (1..10).filter { it % 3 == 0 } 
       .onEach(::print) // Prints: 369 
       .map { it / 3 } 
       .forEach(::print) // Prints: 123 

带有索引和索引变体

有时,元素处理的方式取决于它在列表中的索引。解决这个问题的最通用的方法是使用withIndex函数,它返回带有索引的值列表:

    listOf(9,8,7,6).withIndex() // 1 
       .filter { (i, _) -> i % 2 == 0 } // 2 
       .forEach { (i, v) -> print("$v at $i,") }  
    // Prints: 9 at 0, 7 at 2, 
  1. withIndex函数将每个元素打包成包含元素和其索引的IndexedValue

  2. 在 lambda 中,IndexedValue被解构为索引和值,但是值没有被使用,而是用下划线代替。它可能被省略,但这种方式的代码更易读。这行代码只过滤了偶数索引的元素。

此外,还有不同流处理方法的变体,提供了一个索引:

    val list1 = listOf(2, 2, 3, 3) 
        .filterIndexed { index, _ -> index % 2 == 0 } 
    println(list1) // Prints: [2, 3] 

    val list2 = listOf(10, 10, 10) 
        .mapIndexed { index, i -> index * i } 
    println(list2) // Prints: [0, 10, 20] 

    val list3 = listOf(1, 4, 9) 
        .forEachIndexed { index, i -> print("$index: $i,") } 
    println(list3) // Prints: 0: 1, 1: 4, 2: 9 

sumcountminmaxsorted函数

sum函数计算列表中所有元素的总和。它可以在List<Int>List<Long>List<Short>List<Double>List<Float>List<Byte>上调用:

    val sum = listOf(1,2,3,4).sum() 
    println(sum) // Prints: 10 

通常我们需要对元素的一些属性进行求和,比如对所有用户的点数进行求和。可以通过将用户列表映射到点数列表,然后计算总和来处理:

    class User(val points: Int) 
    val users = listOf(User(10), User(1_000), User(10_000)) 

    val points = users.map { it.points }. sum() 
    println(points) // Prints: 11010 

但是我们通过调用map函数不必要地创建了一个中间集合,直接对点数进行求和会更有效率。为了做到这一点,我们可以使用适当的选择器使用sumBy

    val points = users.sumBy { it.points } 
    println(points) // Prints: 11010

sumBy期望从选择器返回Int,并返回所有元素的总和为Int。如果值不是Int而是Double,那么我们可以使用sumByDouble,它返回Double

    class User(val points: Double) 
    val users = listOf(User(10.0), User(1_000.0), User(10_000.0)) 

    val points = users.sumByDouble { it.points } 
    println(points) // Prints: 11010.0 

count函数提供了类似的功能,当我们需要计算与谓词匹配的元素时使用它:

    val evens = (1..5).count { it % 2 == 1 } 
    val odds = (1..5).count { it % 2 == 0 } 
    println(evens) // Prints: 3 
    println(odds) // Prints: 2 

count函数在没有任何谓词的情况下返回集合或可迭代对象的大小:

    val nums = (1..4).count() 
    println(nums) // Prints: 4   

接下来的重要函数是minmax,它们是返回列表中最小和最大元素的函数。它们可以用于具有自然排序的元素列表(实现Comparable<T>接口)。以下是一个示例:

    val list = listOf(4, 2, 5, 1) 
    println(list.min()) // Prints: 1 
    println(list.max()) // Prints: 5 
    println(listOf("kok", "ada", "bal", "mal").min()) // Prints: ada 

同样,使用sorted函数。它返回一个排序后的列表,但需要在实现Comparable<T>接口的元素集合上调用。以下是一个示例,说明如何使用sorted来获取按字母数字顺序排序的字符串列表:

    val strs = listOf("kok", "ada", "bal", "mal").sorted() 
    println(strs) // Prints: [ada, bal, kok, mal] 

如果项目不可比较怎么办?有两种方法可以对它们进行排序。第一种方法是根据可比较的成员进行排序。我们已经看到一个例子,当我们根据他们的成绩对学生进行排序时:

    students.filter { it.passing } 
       .sortedByDescending { it.grade } 
       .take(3) 
       .forEach(::println) 

在上面的示例中,我们使用可比较的grade属性对学生进行排序。在这里,使用了sortedByDescending,它的工作方式类似于sortedBy,唯一的区别是顺序是降序的(从大到小)。函数内部的选择器可以返回任何可与自身比较的值。以下是一个示例,其中使用String来指定顺序:

    val list = listOf(14, 31, 2) 
    print(list.sortedBy { "$it" }) // Prints: [14, 2, 31] 

类似的函数可以用来根据选择器找到最小和最大的元素:

    val minByLen = listOf("ppp", "z", "as") 
        .minBy { it.length } 
    println(minByLen) // Prints: "z" 

    val maxByLen = listOf("ppp", "z", "as") 
        .maxBy { it.length } 
    println(maxByLen) // Prints: "ppp" 

指定排序顺序的第二种方法是定义一个Comparator,它将确定如何比较元素。接受比较器的函数变体应该有一个With后缀。比较器可以由将 lambda 转换为 SAM 类型的适配器函数来定义:

    val comparator = Comparator<String> { e1, e2 ->  
        e2.length - e1.length  
    } 
    val minByLen = listOf("ppp", "z", "as") 
       .sortedWith(comparator) 
    println(minByLen) // Prints: [ppp, as, z]

Kotlin 还包括用于简化Comparator创建的标准库顶层函数(compareBycompareByDescending)。这是我们如何创建一个比较器,按surnamename按字母顺序对学生进行排序的:

    data class User(val name: String, val surname: String) { 
        override fun toString() = "$name $surname" 
    } 

    val users = listOf( 
       User("A", "A"), 
       User("B", "A"), 
       User("B", "B"), 
       User("A", "B") 
    ) 
    val sortedUsers = users 
       .sortedWith(compareBy({ it.surname }, { it.name })) 

    print(sortedUsers) // [A A, B A, A B, B B] 

注意,我们可以使用属性引用而不是 lambda 表达式:

    val sortedUsers = users 
       .sortedWith(compareBy(User::surname, User::name)) 
    print(sortedUsers) // [A A, B A, A B, B B]  

另一个重要的函数是groupBy,它根据选择器对元素进行分组。groupBy返回Map,即从选择的键到选择映射到以下键的元素列表的映射:

    val grouped = listOf("ala", "alan", "mulan", "malan") 
        .groupBy { it.first() } 
    println(grouped) // Prints: {'a': ["ala", "alan"], "m": ["mulan", "malan"]} 

让我们看一个更复杂的例子。我们需要从每个班级的学生名单中得到最好的学生名单。这是我们如何从学生名单中得到他们的:

    class Student(val name: String, val classCode: String, val meanGrade: Float) 

    val students = listOf( 
       Student("Homer", "1", 1.1F), 
       Student("Carl", "2", 1.5F), 
       Student("Donald", "2", 3.5F), 
       Student("Alex", "3", 4.5F), 
       Student("Marcin", "3", 5.0F), 
       Student("Max", "1", 3.2F) 
    ) 

    val bestInClass = students 
       .groupBy { it.classCode } 
       .map { (_, students) -> students.maxBy { it.meanGrade }!! } 
       .map { it.name } 

    print(bestInClass) // Prints: [Max, Donald, Marcin]

其他流处理函数

有许多不同的流处理函数,没有必要在这里描述它们全部,因为 Kotlin 在其网站上包含了很好的文档。大多数扩展函数的名称都是不言自明的,没有必要真正阅读文档来猜测它们在做什么。在 Android Studio 中,我们可以通过按下Ctrl(mac 上的command键)并点击要阅读其实现的函数来检查真正的实现。

在对可变集合进行处理时,集合处理的重要区别在于,虽然它们可以使用为可变类型定义的额外扩展(MutableIterableMutableCollection),但重要的区别在于改变对象的函数是用现在的祈使形式来表达(例如,sort),而返回具有更改值的新集合的函数通常是用动词的过去形式来表达(例如,sorted)。这里有一个例子:

  • sort:对可变对象进行排序的函数。它返回Unit

  • sorted:返回一个排序后的集合的函数。它不会改变被调用的集合。

    val list = mutableListOf(3,2,4,1) 
    val list2 = list.sorted() 
    println(list) // [3,2,4,1] 
    println(list2) // [1,2,3,4] 
    list.sort() 
    println(list) // [1,2,3,4] 

流集合处理的例子

我们已经看到了一些流处理函数,但是需要一些技巧和创造力来将它们用于复杂的用例。这就是为什么在这一部分,我们将讨论一些复杂的流处理示例。

假设我们再次需要找到根据他们的成绩通过的最好的三名学生。关键区别在于,在这种情况下,学生的最终顺序必须与一开始的顺序相同。请注意,在按成绩排序操作期间,这个顺序会丢失。但是,如果我们保持值和索引在一起,我们可以保留它。由此,我们可以稍后根据保留的索引对元素进行排序。这是如何实现这个处理的:

    data class Student( 
       val name: String, 
       val grade: Double, 
       val passing: Boolean 
    ) 

    val students = listOf( 
       Student("John", 4.2, true), 
       Student("Bill", 3.5, true), 
       Student("John", 3.2, false), 
       Student("Aron", 4.3, true), 
       Student("Jimmy", 3.1, true) 
    ) 

    val bestStudents = students.filter { it.passing } // 1 
       .withIndex() // 2 
       .sortedBy { it.value.grade } // 3 
       .take(3) // 4 
       .sortedBy { it.index } // 5 
       .map { it.value } // 6 

    // Print list of names 
    println(bestStudents.map { it.name }) // [John, Bill, Jimmy] 
  1. 过滤以保留只有通过的学生

  2. 添加索引以能够重现元素顺序

  3. 根据他们的成绩对学生进行排序

  4. 只取最好的 10 名学生

  5. 通过排序重现顺序

  6. 将值与索引映射到只有值

请注意,这个实现是简洁的,对集合执行的每个操作都可以逐行轻松阅读。

集合流处理的重大优势在于它易于管理这个过程的复杂性。我们知道大多数操作的复杂度,如mapfilter,是O(n),排序操作的复杂度是O(nlog(n))。流操作的复杂度是每个步骤的最大复杂度,因此上述处理的复杂度是O(nlog(n)),因为sortedBy是具有最大复杂度的步骤。

作为下一个例子,假设我们有一个包含不同类别中玩家结果的列表:

    class Result( 
       val player: Player, 
       val category: Category, 
       val result: Double 
    ) 
    class Player(val name: String) 
    enum class Category { SWIMMING, RUNNING, CYCLING } 

我们有一些示例数据:

    val results = listOf( 
       Result("Alex", Category.SWIMMING, 23.4), 
       Result("Alex", Category.RUNNING, 43.2), 
       Result("Alex", Category.CYCLING, 15.3), 
       Result("Max", Category.SWIMMING, 17.3), 
       Result("Max", Category.RUNNING, 33.3), 
       Result("Bob", Category.SWIMMING, 29.9), 
       Result("Bob", Category.CYCLING, 18.0) 
    ) 

这是我们如何在每个类别中找到最好的玩家:

    val bestInCategory = results.groupBy { it.category } // 1 
       .mapValues { it.value.maxBy { it.result }?.player } // 2 
    print(bestInCategory)  
    // Prints: {SWIMMING=Bob, RUNNING=Alex, CYCLING=Bob} 
  1. 我们将结果分组到类别中。返回类型是Map<Category>List<Result>

  2. 我们正在映射map函数的值。在内部,我们找到了这个类别中的最佳结果,并且我们正在取得与这个结果相关联的玩家。mapValues函数的返回值是Map<Category>Player?>

前面的例子展示了在 Kotlin 中如何通过集合处理函数轻松解决与集合相关的复杂问题。在使用 Kotlin 一段时间后,大多数这些函数对程序员来说都很熟悉,然后集合处理问题就变得相当容易解决。当然,像上面介绍的那样复杂的函数很少见,但是简单的、几步的处理在日常编程中是相当常见的。

序列

Sequence是一个接口,也用于引用元素的集合。它是Iterable的替代品。对于Sequence,大多数集合处理函数(mapflatMapfiltersorted等)都有单独的实现。关键区别在于,所有这些函数都是以这样的方式构造的,它们返回序列,该序列包装在前一个序列上。由于这个原因,以下几点成为真实:

  • 序列的大小不需要事先知道

  • 序列处理更有效,特别是对于大型集合,我们想要执行多个转换(详细信息将在后面描述)

在 Android 中,序列用于处理非常大的集合或处理事先不知道大小的元素(例如读取可能很长的文档的行)。有不同的创建序列的方式,但最简单的是在Iterable上调用asSequence函数,或者使用sequenceOf顶级函数来制作类似列表的序列。

序列的大小不需要事先知道,因为值是在需要时计算的。这是一个例子:

    val = generateSequence(1) { it + 1 } // 1\. Instance of GeneratorSequence 
       .map { it * 2 } // 2\. Instance of TransformingSequence 
       .take(10) // 3\. Instance of  TakeSequence 
       .toList() // 4\. Instance of List 

    println(numbers) // Prints: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
  1. 函数generateSequence是序列生成的一种方式。这个序列包含从 1 到无穷大的下一个数字。

  2. map函数将一个序列打包到另一个序列中,它从第一个序列中获取值,然后计算转换后的值。

  3. 函数take(10)还会将一个序列打包到另一个序列中,该序列在第 10 个元素结束。如果没有这行代码的执行,当我们在一个无限序列上操作时,处理时间将是无限的。

  4. 最后,函数toList正在处理每个值并返回最终列表。

重要的是要强调,在最后一步(在终端操作中)元素是一个接一个地处理的。让我们看另一个例子,其中每个操作也打印值以进行日志记录。让我们从以下代码开始:

    val seq = generateSequence(1) { println("Generated ${it+1}"); it + 1 } 
       .filter { println("Processing of filter: $it"); it % 2 == 1 } 
       .map { println("Processing map: $it"); it * 2 } 
       .take(2) 

控制台会打印什么?绝对什么都不会打印。没有计算出任何值。原因是所有这些中间操作都是惰性的。要检索结果,我们需要使用一些终端操作,比如toList。让我们使用以下操作:

    seq.toList() 

然后我们会在控制台中看到以下内容:

Processing of filter: 1 
Processing map: 1 
Generated 2 
Processing of filter: 2 
Generated 3 
Processing of filter: 3 
Processing map: 3 

请注意,元素是一个接一个地完全处理的。在标准列表处理中,操作的顺序将完全不同:

    (1..4).onEach { println("Generated $it") } 
       .filter { println("Processing filter: $it"); it % 2 == 1 } 
       .map { println("Processing map: $it"); it * 2 } 

前面的代码打印如下:

Generated 1 
Generated 2 
Generated 3 
Generated 4 
Processing filter: 1 
Processing filter: 2 
Processing filter: 3 
Processing filter: 4 
Processing map: 1 
Processing map: 3 

这解释了为什么序列比经典的集合处理更有效--不需要在中间步骤中创建集合。值是按需逐个处理的。

带接收者的函数文字

就像函数有一个函数类型,允许它们被保存为对象一样,扩展函数也有它们的类型,允许它们以这种方式被保存。它被称为带接收者的函数类型。它看起来像简单的函数类型,但接收者类型位于参数之前(就像在扩展定义中一样):

    var power: Int.(Int) -> Int

引入带接收者的函数类型使函数和类型之间具有完全的内聚性,因为现在所有函数都可以表示为对象。它可以使用带接收者的 lambda 表达式或带接收者的匿名函数来定义。

在带接收者的 lambda 表达式定义中,唯一的区别是我们可以通过this引用接收者,并且可以明确使用接收者元素。对于 lambda 表达式,必须在参数中指定类型,因为没有语法来指定接收者类型。这里将power定义为带接收者的 lambda 表达式:

    power = { n -> (1..n).fold(1) { acc, _ -> this * acc } } 

匿名函数还允许我们定义接收者,并且其类型放在函数名之前。在这样的函数中,我们可以在主体内部使用this来引用扩展接收者对象。请注意,匿名扩展函数正在指定接收者类型,因此可以推断属性类型。这里将power定义为匿名扩展函数:

    power = fun Int.(n: Int) = (1..n).fold(1) { acc, _ -> this * acc } 

带接收者的函数类型可以像接收者类型的方法一样使用:

    val result = 10.power(3) 
    println(result) // Prints: 1000 

函数类型最常用作函数参数。这里有一个示例,其中参数函数用于在创建元素后配置元素:

    fun ViewGroup.addTextView(configure: TextView.()->Unit) { 
        val view = TextView(context) 
        view.configure() 

        addView(view) 

    }

    // Usage 
    val linearLayout = findViewById(R.id.contentPanel) as LinearLayout 

    linearLayout.addTextView { // 1 
        text = "Marcin" // 2 
        textSize = 12F // 2 
    }
  1. 在这里,我们使用 lambda 表达式作为参数。

  2. 在 lambda 表达式中,我们可以直接调用接收者方法。

Kotlin 标准库函数

Kotlin stdlib 提供了一组扩展函数(letapplyalsowithrunto),具有通用的非受限接收者(通用类型没有限制)。它们是小巧方便的扩展,了解它们非常有利,因为它们在所有 Kotlin 项目中都非常有用。其中一个函数let在第二章中简要介绍过,我们看到它如何可以用作对空值检查的替代方法:

    savedInstanceState?.let{ state ->  
        println(state.getBoolean("isLocked"))
    } 

let所做的就是调用指定的函数并返回其结果。在上面的示例中,它与安全调用运算符一起使用,只有当属性savedInstanceState不为 null 时才会调用。let函数实际上只是一个带有参数函数的通用扩展函数:

    inline fun <T, R> T.let(block: (T) -> R): R = block(this) 

在 stdlib 中,有更多类似于let的函数。这些函数是applyalsowithrun。它们很相似,所以我们将一起描述它们。以下是其余函数的定义:

inline fun <T> T.apply(block: T.() -> Unit): T { 

    block(); 

    return this 

} 
inline fun <T> T.also(block: (T) -> Unit): T { 

    block(this); 

    return this 

} 
inline fun <T, R> T.run(block: T.() -> R): R = block() 
inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() 

让我们看看使用示例:

    val mutableList = mutableListOf(1) 

    val mutableList = mutableListOf(1)

    val letResult = mutableList.let {

        it.add(2)

        listOf("A", "B", "C")

    }

    println(letResult) // Prints: [A, B, C]

    val applyResult = mutableList.apply {

        add(3)

        listOf("A", "B", "C")

    }

    println(applyResult) // Prints: [1, 2, 3]

    val alsoResult = mutableList.also {

       it.add(4)

       listOf("A", "B", "C")

    }

    println(alsoResult) // Prints: [1, 2, 3, 4]

    val runResult = mutableList.run {

       add(5)

       listOf("A", "B", "C")

    }

    println(runResult) // Prints: [A, B, C]

    val withResult = with(mutableList) {

       add(6)

       listOf("A", "B", "C")

    }

    println(withResult) // Prints: [A, B, C]

    println(mutableList) // Prints: [1, 2, 3, 4, 5, 6]

这些差异总结在以下表中:

返回的对象/参数函数类型 带接收者的函数文字(接收对象表示为this 函数文字(接收对象表示为it
接收对象 apply also
函数文字的结果 run /with let

虽然这些函数很相似,并且在许多情况下可以互换使用,但有一些约定规定了哪些函数更适用于特定的用例。

let 函数

当我们想要在流处理中将标准函数用作扩展函数时,首选let函数:

    val newNumber = number.plus(2.0) 
       .let { pow(it, 2.0) } 
       .times(2) 

与其他扩展一样,它可以与安全调用运算符结合使用:

    val newNumber = number?.plus(2.0) 
       ?.let { pow(it, 2.0) } 

当我们只想要解包可空的可读写属性时,也更倾向于使用let函数。在这种情况下,无法智能转换此属性,我们需要对其进行遮蔽,就像在这个解决方案中一样:

    var name: String? = null 

    fun Context.toastName() { 
        val name = name
        if(name != null) { 
            toast(name) 
        } 
    }   

name变量是遮蔽属性名称,如果 name 是可读写属性,则这是必要的,因为智能转换只允许在可变或局部变量上。

我们可以用let的用法和安全调用运算符来替换前面的代码:

    name?.let { setNewName(it) } 

请注意,使用 Elvis 运算符,当namenull时,我们可以轻松添加return或异常抛出:

    name?.let { setNewName(it) } ?: throw Error("No name setten") 

类似的方式,let可以用作以下语句的替代:

    val comment = if(field == null) getComment(field) else "No comment 

使用let函数的实现将如下所示:

    val comment = field?.let { getComment(it) } ?: "No comment" 

这种方式使用的let函数在转换接收器的方法链中是首选的:

    val text = "hello {name}" 

    fun correctStyle(text: String) = text 
       .replace("hello", "hello,") 

    fun greet(name: String) { 
        text.replace("{name}", name) 
           .let { correctStyle(it) } 
           .capitalize() 
           .let { print(it) } 
    } 

    // Usage 
    greet("reader") // Prints: Hello, reader 

我们还可以通过将函数引用作为参数来使用更简单的语法:

    text.replace("{name}", name) 
       .let(::correctStyle) 
       .capitalize() 
       .let(::print)

使用 apply 函数进行初始化

有时我们需要通过调用一些方法或修改一些属性来创建和初始化对象,比如当我们创建一个Button时:

    val button = Button(context) 
    button.text = "Click me"   
    button.isVisible = true 
    button.setOnClickListener { /* ... */ } 
    this.button = button 

我们可以通过使用apply扩展函数来减少代码冗长。我们可以从button作为接收对象的上下文中调用所有这些方法:

    button = Button(context).apply { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

also 函数

also函数与apply类似,唯一的区别是参数函数接受一个参数而不是一个接收器。当我们想对一个对象进行一些不是初始化的操作时,它是首选的:

    abstract class Provider<T> { 

       var original: T? = null 
       var override: T? = null 

       abstract fun create(): T 

       fun get(): T = override ?: original ?: create().also { original = it } 
    } 

当我们需要在处理过程中进行一些操作时,例如在使用构建器模式构建对象时,also函数也是首选的:

    fun makeHttpClient(vararg interceptors: Interceptor) =  
        OkHttpClient.Builder() 
            .connectTimeout(60, TimeUnit.SECONDS) 
            .readTimeout(60, TimeUnit.SECONDS) 
            .also { it.interceptors().addAll(interceptors) } 
            .build() 

also更受欢迎的另一种情况是当我们已经在扩展函数中,而且不想添加另一个扩展接收器时:

    class Snail { 
        var name: String = "" 
        var type: String = "" 

        fun greet() { 
            println("Hello, I am $name") 
        } 
    } 

    class Forest { 
        var members = listOf<Sneil>() 

       fun Sneil.reproduce(): Sneil = Sneil().also { 
           it.name = name 
           it.type = type 
           members += it 
       } 
    } 

run 和 with 函数

runwith函数都接受带有接收器的 lambda 文字作为参数,并返回其结果。它们之间的区别在于run接受一个接收器,而with函数不是扩展函数,它将我们正在操作的对象作为参数。当我们设置对象时,这两个函数都可以作为apply函数的替代品:

    val button = findViewById(R.id.button) as Button 

    button.apply { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

    button.run { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

    with(button) { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

applyrunwith之间的区别在于apply返回一个接收对象,而runwith返回函数文字的结果。尽管当我们需要其中任何一个时,我们应该选择返回它的函数。当我们不需要任何返回值时,应该使用哪一个是有争议的。大多数情况下,建议使用runwith函数,因为also更常用于需要返回值的情况。

关于runwith函数之间的区别:当值可为空时,应该使用run函数而不是with函数,因为这样我们可以使用安全调用或非空断言:

    val button = findViewById(R.id.button) as? Button 

    button?.run { 
        text = "Click me" 
        isVisible = true 
       setOnClickListener { /* ... */ } 
    } 

当表达式很短时,with函数优于run


    val button = findViewById(R.id.button) as Button 

    with(button) { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

另一方面,当表达式很长时,run优于with

    itemAdapter.holder.button.run { 
        text = "Click me" 
        isVisible = true 
        setOnClickListener { /* ... */ } 
    } 

to 函数

infix函数是在第四章,类和对象中引入的,但它们不仅可以定义为成员类,还可以定义为扩展函数。这使得可以为任何对象创建一个infix扩展函数。其中一种这样的扩展函数是to,它在第二章,打下基础中简要描述过。现在我们有了理解其实现所需的知识。这是to的定义:

    infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) 

这样可以在任何两个对象之间放置to,并以这种方式与它们创建Pair

    println( 1 to 2 == Pair(1, 2) ) // Prints: true 

注意,我们可以创建infix扩展函数的事实使我们可以将infix函数定义为任何类型的扩展。这是一个例子:

    infix fun <T> List<T>.intersection(other: List<T>) 
        = filter { it in other } 

    listOf(1, 2, 3) intersection listOf(2, 3, 4) // [2,3] 

领域特定语言

诸如带有接收器的 lambda 文字和成员扩展函数之类的功能使得可以定义类型安全的构建器,这些构建器来自 Groovy。最著名的 Android 示例是 Gradle 配置,build.gradle,目前是用 Groovy 编写的。这些构建器是 XML、HTML 或配置文件的良好替代品。与 Kotlin 的优势是我们可以使这些配置完全类型安全,并提供更好的 IDE。这样的构建器是 Kotlin领域特定语言DSL)的一个例子。

Android 中最流行的 Kotlin DSL 模式是可选回调类的实现。它用于解决回调接口缺乏对多个方法的功能支持的问题。传统上,实现需要使用对象表达式,就像下面的例子中一样:

searchView.addTextChangedListener(object : TextWatcher { 
  override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} 

  override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { 
    presenter.onSearchChanged(s.toString()) 
  } 

  override fun afterTextChanged(s: Editable) {} 
}) 

这种实现的主要问题如下:

  • 我们需要实现接口中的所有方法

  • 需要为每个方法实现函数结构

  • 我们需要使用对象表达式

让我们定义以下类,将回调作为可变属性保存:

class TextWatcherConfig : TextWatcher { 

  private var beforeTextChangedCallback: (BeforeTextChangedFunction)? = null // 1 
  private var onTextChangedCallback: (OnTextChangedFunction)? = null // 1 
  private var afterTextChangedCallback: (AfterTextChangedFunction)? = null // 1 

  fun beforeTextChanged(callback: BeforeTextChangedFunction){  // 2 
    beforeTextChangedCallback = callback 
  } 

  fun onTextChanged(callback: OnTextChangedFunction) { // 2 
    onTextChangedCallback = callback 
  } 

  fun afterTextChanged(callback: AfterTextChangedFunction) { // 2 
    afterTextChangedCallback = callback 
  } 

  override fun beforeTextChanged (s: CharSequence?, start: Int, count: Int, 

  after: Int) { // 3 

    beforeTextChangedCallback?.invoke(s?.toString(), start, count, after) // 4 
  } 

  override fun onTextChanged(s: CharSequence?, start: Int, before: 

  Int, count: Int) { // 3 
    onTextChangedCallback?.invoke(s?.toString(), start, before, count) // 4 

  } 

  override fun afterTextChanged(s: Editable?) { // 3 
    afterTextChangedCallback?.invoke(s) 
  } 
} 

private typealias BeforeTextChangedFunction = 

(text: String?, start: Int, count: Int, after: Int)->Unit 

private typealias OnTextChangedFunction = 

(text: String?, start: Int, before: Int, count: Int)->Unit 

private typealias AfterTextChangedFunction = 

(s: Editable?)->Unit 

  1. 当调用任何重写的函数时使用的回调。

  2. 用于设置新回调的函数。它们的名称对应于处理程序函数的名称,但它们包括回调作为参数。

  3. 每个事件处理函数只是在回调存在时调用回调。

  4. 为了简化使用,我们还改变了类型,原始方法中的CharSequence被改为了String

现在我们需要的是一个扩展函数,它将简化回调配置。它的名称不能与TextView的任何名称相同,但我们需要做的只是做一个小修改:

fun TextView.addOnTextChangedListener(config: TextWatcherConfig.()->Unit) { 
    val textWatcher = TextWatcherConfig() 
    textWatcher.config() 
    addTextChangedListener(textWatcher) 
} 

有了这样的定义,我们可以这样定义我们需要的回调:

    searchView.addOnTextChangedListener { 
       onTextChanged { text, start, before, count ->  
           presenter.onSearchChanged(text)  
       } 
    } 

我们使用下划线来隐藏未使用的参数,以改进我们的实现:

    searchView.addOnTextChangedListener { 
        onTextChanged { text, _, _, _ -> 
            presenter.onSearchChanged(text) 
        } 
    } 

现在另外两个回调beforeTextChangedafterTextChanged被忽略了,但我们仍然可以添加其他实现:

    searchView.addOnTextChangedListener { 
        beforeTextChanged { _, _, _, _ -> 
            Log.i(TAG, "beforeTextChanged invoked") 
        } 
        onTextChanged { text, _, _, _ -> 
            presenter.onSearchChanged(text) 
        } 
        afterTextChanged { 
            Log.i(TAG, "beforeTextChanged invoked") 
        } 
    } 

以这种方式定义的监听器具有以下属性:

  • 它比对象表达式实现更短

  • 它包括默认函数实现

  • 它允许我们隐藏未使用的参数

在 Android SDK 中,有多个具有多个处理程序的监听器,可选回调类的 DSL 实现在 Android 项目中非常流行。类似的实现也可以在库中找到,比如前面提到的 Anko。

另一个例子是 DSL,它将用于定义布局结构,而不使用 XML 布局文件。我们将定义一个函数来添加和配置LinearLayoutTextView,并使用它来定义一个简单的视图。

    fun Context.linearLayout(init: LinearLayout.() -> Unit): LinearLayout { 
        val layout = LinearLayout(this) 
        layout.layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 
        layout.init() 
        return layout 
    } 

    fun ViewGroup.linearLayout(init: LinearLayout.() -> Unit): LinearLayout { 
        val layout = LinearLayout(context) 
        layout.layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 
        layout.init() 
        addView(layout) 
        return layout  
    } 

    fun ViewGroup.textView(init: TextView.() -> Unit): TextView { 
        val layout = TextView(context) 
        layout.layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 
        layout.init() 
        addView(layout) 
        return layout 
    } 

    // Usage 
    class MainActivity : AppCompatActivity() { 

       override fun onCreate(savedInstanceState: Bundle?) { 
           super.onCreate(savedInstanceState) 
           val view = linearLayout { 
               orientation = LinearLayout.VERTICAL 
               linearLayout { 
                   orientation = LinearLayout.HORIZONTAL 
                   textView { text = "A" } 
                   textView { text = "B" } 
               } 
               linearLayout { 
                   orientation = LinearLayout.HORIZONTAL 
                   textView { text = "C" } 
                   textView { text = "D" } 
               } 
           } 
           setContentView(view) 
       } 
    } 

我们还可以从头开始定义我们自己的自定义 DSL。让我们制作一个简单的 DSL,定义一系列文章。我们知道每篇文章应该在不同的类别中定义,并且文章有其名称、URL 和标签。我们想要实现的是以下定义:

   category("Kotlin") {

       post {

           name = "Awesome delegates"

           url = "SomeUrl.com"

       }

       post {

           name = "Awesome extensions"

           url = "SomeUrl.com"

       }

   }

   category("Android") {

       post {

           name = "Awesome app"

           url = "SomeUrl.com"

           tags = listOf("Kotlin", "Google Login")

       }

    }

这里最简单的对象是Post类。它保存帖子属性并允许它们被更改:

    class Post { 
        var name: String = "" 
        var url: String = "" 
        var tags: List<String> = listOf() 
    } 

接下来,我们需要定义一个类来保存类别。它需要存储一系列帖子,还需要包含其名称。还必须定义一个函数,允许简单的帖子添加。这个函数需要包含一个函数参数,其中Post是接收者类型。以下是定义:

    class PostCategory(val name: String) { 
        var posts: List<Post> = listOf() 

        fun post(init: Post.()->Unit) { 
            val post = Post() 
            post.init() 
            posts += post 
        } 
    } 

此外,我们需要一个类来保存类别列表,并允许简单的类别定义:

    class PostList { 

        var categories: List<PostCategory> = listOf() 

        fun category(name: String, init: PostCategory.()->Unit) { 
            val category = PostCategory(name) 
            category.init() 
            categories += category 
        } 
    } 

现在我们需要的是definePosts函数,其定义可能如下:

    fun definePosts(init: PostList.()->Unit): PostList { 
        val postList = PostList() 
        postList.init() 
        return postList 
    } 

这就是我们需要的全部。现在我们可以通过一个简单的类型安全构建器来定义对象结构:

val postList = definePosts { 
   category("Kotlin") { 
       post { 
           name = "Awesome delegates" 
           url = "SomeUrl.com" 
       } 
       post { 
           name = "Awesome extensions" 
           url = "SomeUrl.com" 
       } 
   } 
   category("Android") { 
       post { 
           name = "Awesome app" 
           url = "SomeUrl.com" 
           tags = listOf("Kotlin", "Google Login") 
       } 
   } 
} 

DSL 是一个非常强大的概念,在 Kotlin 社区中越来越受欢迎。由于库的存在,已经可以使用 Kotlin DSL 完全替代以下内容:

  • Android 布局文件(Anko)

  • Gradle 配置文件

  • HTML 文件(kotlinx.html

  • JSON 文件(Kotson)

还有很多其他的配置文件。让我们看一个定义了 Kotlin DSL 以提供类型安全构建器的示例库。

Anko

Anko 是一个库,提供了 DSL 来定义 Android 视图,而无需任何 XML 布局。这与我们已经看到的示例非常相似,但 Anko 使得完全可以从项目中删除 XML 布局文件成为可能。这里是一个用 Anko DSL 编写的示例视图:

    verticalLayout { 
        val name = editText() 
        button("Say Hello") { 
            onClick { toast("Hello, ${name.text}!") } 
        } 
    } 

这就是结果:

github.com/Kotlin/anko

使用 Anko DSL 也可以定义更复杂的布局。这些视图可以放置在作为视图的自定义类上,甚至直接放在onCreate方法中:

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 

       verticalLayout { 
           padding = dip(30) 
           editText { 
               hint = "Name" 
               textSize = 24f 
           } 
           editText { 
                hint = "Password" 
                textSize = 24f 
           } 
           button("Login") { 
              textSize = 26f 
           } 
       }  
    } 

要了解更多关于这个例子的信息,您可以访问 Anko Wiki 网站github.com/Kotlin/anko/wiki/Anko-Layouts

目前还有争议,DSL 布局定义是否会取代 XML 定义。在撰写本文时,以这种方式定义视图并不那么流行,因为缺乏来自 Google 的支持,但是 Google 宣布他们将支持 Kotlin,因此这个想法有可能变得更加流行,基于 DSL 的布局也可能会得到更多支持,甚至有朝一日成为通用的。

总结

在本章中,我们讨论了 Kotlin 扩展函数和属性,包括顶层定义和类型成员定义。我们看到了 Kotlin 标准库扩展函数如何简化集合处理和执行各种操作。我们还描述了带有接收者的函数类型以及带有接收者的函数文字。此外,我们还看到了标准库中一些重要的通用函数,它们使用了扩展:letapplyalsowithrunto。最后,我们看到了 DSL 如何在 Kotlin 中定义以及它的用途。

在下一章中,将介绍在 Java 世界中不存在的下一个功能,它在 Kotlin 开发中提供了非常大的可能性--类和属性委托。

第八章:代理

Kotlin 非常重视设计模式。之前,我们已经看到了单例模式的使用是如何通过对象声明简化的,以及观察者模式的使用是如何通过高阶函数和函数类型变得微不足道的。此外,Kotlin 通过 lambda 表达式和函数类型简化了大多数函数模式的使用。在本章中,我们将看到委托和装饰器模式的使用是如何通过类委托简化的。我们还将看到一个在编程世界中非常新的特性——属性委托——以及它是如何用来使 Kotlin 属性更加强大的。

在本章中,我们将涵盖以下主题:

  • 委托模式

  • 类委托

  • 装饰器模式

  • 属性委托

  • 标准库中的属性委托

  • 创建自定义属性委托

类委托

Kotlin 有一个名为类委托的特性。这是一个非常不起眼的特性,但有许多实际应用。值得注意的是,它与两种设计模式——委托模式和装饰器模式——密切相关。我们将在接下来的章节中更详细地讨论这些模式。委托和装饰器模式已经为人所知多年,但在 Java 中,它们的实现需要大量样板代码。Kotlin 是第一批为这些模式提供本地支持并将样板代码减少到最低程度的语言之一。

委托模式

在面向对象编程中,委托模式是一种设计模式,它是继承的一种替代方法。委托意味着对象通过将请求委托给另一个对象(委托)来处理请求,而不是扩展类。

为了支持从 Java 中所知的多态行为,两个对象都应该实现相同的接口,该接口包含所有委托的方法和属性。委托模式的一个简单示例如下:

    interface Player { // 1 
        fun playGame() 
    } 

    class RpgGamePlayer(val enemy: String) : Player { 
        override fun playGame() { 
            println("Killing $enemy") 
        } 
    } 

    class WitcherPlayer(enemy: String) : Player { 
        val player = RpgGamePlayer(enemy) // 2 

        override fun playGame() { 
            player.playGame() // 3 
        } 
    } 

    // Usage 
    RpgGamePlayer("monsters").playGame() // Prints: Killing monsters 
    WitcherPlayer("monsters").playGame() // Prints: Killing monsters 
  1. 当我们谈论类委托时,需要有一个定义了委托方法的接口。

  2. 我们要委托的对象(委托)。

  3. WitcherPlayer类中的所有方法都应该调用委托对象(player)上的相应方法。

这被称为委托,因为WitcherPlayer类将Player接口中定义的方法委托给了RpgGamePlayer类型的实例(player)。使用继承而不是委托也可以达到类似的结果。它看起来如下:

    class WitcherPlayer() : RpgGamePlayer() 

乍一看,这两种方法可能看起来相似,但委托和继承有很多不同之处。一方面,继承更受欢迎,使用更为普遍。它经常在 Java 中使用,并与多种面向对象模式相关联。另一方面,有一些来源强烈支持委托。例如,影响深远的《设计模式》一书,由四人组合编写,包含了这样的原则:更倾向于对象组合而不是类继承。此外,流行的《Effective Java》一书中包含了这样的规则:更倾向于组合而不是继承(第 6 条)。它们都强烈支持委托模式。以下是一些支持使用委托模式而不是继承的基本论点:

  • 通常类并不是为了继承而设计的。当我们重写方法时,我们并不知道关于类内部行为的基本假设(方法何时被调用,这些调用如何影响对象、状态等)。例如,当我们重写方法时,我们可能不知道它被其他方法使用,因此重写的方法可能会被超类意外调用。即使我们检查方法何时被调用,这种行为也可能在类的新版本中发生变化(例如,如果我们从外部库扩展类),从而破坏我们子类的行为。非常少量的类被正确设计和记录为继承,但几乎所有非抽象类都是为使用而设计的(这包括委托)。

  • 在 Java 中,可以将一个类委托给多个类,但只能继承一个。

  • 通过接口,我们指定要委托的方法和属性。这与接口隔离原则(来自 SOLID 原则)兼容--我们不应该向客户端公开不必要的方法。

  • 有些类是 final 的,所以我们只能委托给它们。事实上,所有不设计用于继承的类都应该是 final 的。Kotlin 的设计者意识到了这一点,并且默认情况下将 Kotlin 中的所有类都设为 final。

  • 将类设为 final 并提供适当的接口是公共库的良好实践。我们可以更改类的实现而不必担心会影响库的用户(只要从接口的角度来看行为是相同的)。它们不可继承,但仍然是很好的委托候选者。

有关如何设计支持继承的类以及何时应使用委托的更多信息可以在书籍Effective Java中找到,在Item 16: Favor composition over inheritance中找到。

当然,使用委托而不是继承也有缺点。以下是主要问题:

  • 我们需要创建指定应该委托哪些方法的接口

  • 我们无法访问受保护的方法和属性

在 Java 中,使用继承还有一个更有力的论据:它要容易得多。即使比较我们WitcherPlayer示例中的代码,我们也可以看到委托需要大量额外的代码:

     class WitcherPlayer(enemy: String) : Player { 
         val player = RpgGamePlayer(enemy)    
         override fun playGame() { 
             player.playGame() 
         } 
     } 

     class WitcherPlayer() : RpgGamePlayer() 

当我们处理具有多个方法的接口时,这是特别棘手的。幸运的是,现代语言重视委托模式的使用,并且许多语言都具有本地类委托支持。Swift 和 Groovy 对委托模式有很强的支持,Ruby、Python、JavaScript 和 Smalltalk 也通过其他机制支持。Kotlin 也强烈支持类委托,并且使用这种模式非常简单,几乎不需要样板代码。例如,示例中的WitcherPlayer类可以在 Kotlin 中以这种方式实现:

    class WitcherPlayer(enemy: String) : Player by RpgGamePlayer(enemy) {} 

使用by关键字,我们通知编译器将WitcherPlayer中定义的Player接口的所有方法委托给RpgGamePlayer。在WitcherPlayer构造期间创建了一个RpgGamePlayer的实例。简单来说:WitcherPlayer将在Player接口中定义的方法委托给一个新的RpgGamePlayer对象。

这里真正发生的是,在编译期间,Kotlin 编译器从PlayerWitcherPlayer中生成了未实现的方法,并用对RpgGamePlayer实例的调用填充它们(就像我们在第一个示例中实现的那样)。最大的改进是我们不需要自己实现这些方法。还要注意的是,如果委托方法的签名发生变化,那么我们不需要更改所有委托给它的对象,因此类更容易维护。

还有另一种创建和保存委托实例的方法。它可以由构造函数提供,就像这个例子中一样:

    class WitcherPlayer(player: Player) : Player by player 

我们还可以委托给构造函数中定义的属性:

    class WitcherPlayer(val player: Player) : Player by player 

最后,我们可以委托给在类声明期间可访问的任何属性:

    val d = RpgGamePlayer(10) 
    class WitcherPlayer(a: Player) : Player by d 

此外,一个对象可以有多个不同的委托:

    interface Player { 
        fun playGame() 
    } 

    interface GameMaker { // 1 
        fun developGame() 
    } 

    class WitcherPlayer(val enemy: String) : Player { 
        override fun playGame() { 
            print("Killin $enemy! ") 
        } 
    } 

    class WitcherCreator(val gameName: String) : GameMaker{ 
        override fun developGame() { 
            println("Makin $gameName! ") 
        } 
    } 

    class WitcherPassionate : 
        Player by WitcherPlayer("monsters"), 
        GameMaker by WitcherCreator("Witcher 3") { 

        fun fulfillYourDestiny() { 
            playGame() 
            developGame() 
        } 
    } 

    // Usage 
    WitcherPassionate().fulfillYourDestiny() // Killin monsters! Makin Witcher 3! 
  1. WitcherPlayer类将Player接口委托给一个新的RpgGamePlayer对象,GameMaker委托给一个新的WitcherCreator对象,并且还包括fulfillYourDestiny函数,该函数使用了来自两个委托的函数。请注意,WitcherPlayerWitcherCreator都没有标记为 open,没有这个标记,它们就不能被扩展。但它们可以被委托。

有了这样的语言支持,委托模式比继承更有吸引力。虽然这种模式既有优点又有缺点,但知道何时应该使用它是很好的。应该使用委托的主要情况如下:

  • 当你的子类违反了里氏替换原则;例如,当我们处理继承仅用于重用超类代码的情况,但它实际上并不像那样工作。

  • 当子类只使用超类的部分方法时。在这种情况下,只是时间问题,直到有人调用了他们本不应该调用的超类方法。使用委托,我们只重用我们选择的方法(在接口中定义)。

  • 当我们不能或者不应该继承时,因为:

  • 这个类是 final 的

  • 它不可访问,也不可从接口后面使用

  • 它只是不适合继承

请注意,虽然 Kotlin 中的类默认是 final 的,但大多数类都将保持 final。如果这些类放在库中,那么我们很可能无法更改或打开这个类。委托将是唯一的选择,以创建具有不同行为的类。

里氏替换原则是面向对象编程中的一个概念,它规定所有子类应该像它们的超类一样工作。简单来说,如果某个类的单元测试通过,那么它的子类也应该通过。这个原则由 Robert C. Martin 推广,他将其列为最重要的面向对象编程规则之一,并在流行的书籍Clean Code中描述了它。

《Effective Java》一书指出:“只有在子类真正是超类的子类型的情况下才适合使用继承。”换句话说,只有当类B扩展类A时,两个类之间存在is-a关系。如果你想让类B扩展类A,问问自己“每个 B 都是一个 A 吗?”在接下来的部分,该书建议在其他所有情况下应该使用组合(最常见的实现是委托)。

值得注意的是,Cocoa(苹果的 UI 框架,用于构建在 iOS 上运行的软件程序)很常用委托而不是继承。这种模式变得越来越流行,在 Kotlin 中得到了很好的支持。

装饰器模式

另一个常见的情况是,当我们实现装饰器模式时,Kotlin 类委托非常有用。装饰器模式(也称为包装器模式)是一种设计模式,它使得可以在不使用继承的情况下向现有类添加行为。与扩展不同,我们可以在不修改对象的情况下添加新行为,装饰器模式使用委托,但是以一种非常特定的方式--委托是从类的外部提供的。经典结构如下 UML 图所示:

装饰器模式的经典实现的 UML 图。来源:upload.wikimedia.org

装饰器包含它装饰的对象,同时实现相同的接口。

来自 Java 世界的装饰器使用最广泛的例子是InputStream。有不同类型的类型扩展了InputStream,还有很多装饰器可以用来为它们添加功能。这个装饰器可以用来添加缓冲,获取压缩文件的内容,或者将文件内容转换为 Java 对象。让我们看一个使用多个装饰器来读取一个压缩的 Java 对象的例子:

    // Java 
    FileInputStream fis = new FileInputStream("/someFile.gz"); // 1 
    BufferedInputStream bis = new BufferedInputStream(fis); // 2 
    GzipInputStream gis = new GzipInputStream(bis); // 3 
    ObjectInputStream ois = new ObjectInputStream(gis); // 4 
    SomeObject someObject = (SomeObject) ois.readObject(); // 5 
  1. 创建一个用于读取文件的简单流。

  2. 创建一个包含缓冲的新流。

  3. 创建一个包含读取 GZIP 文件格式中压缩数据功能的新流。

  4. 创建一个新的流,添加反序列化原始数据和之前使用ObjectOutputStream写入的对象的功能。

  5. 流在ObjectInputStreamreadObject方法中使用,但是这个例子中的所有对象都实现了InputStream(这使得可以以这种方式打包它),并且可以通过这个接口指定的方法来读取。

请注意,这种模式也类似于继承,但我们可以决定我们想要使用哪些装饰器以及以什么顺序。这样更加灵活,并在使用过程中提供更多可能性。一些人认为,如果设计者能够制作一个具有所有设计功能的大类,然后使用方法来打开或关闭其中的一些功能,那么InputStream的使用会更好。这种方法将违反单一责任原则,并导致更加复杂和不太可扩展的代码。

尽管装饰器模式被认为是实际应用中最好的模式之一,但在 Java 项目中很少被使用。这是因为实现并不简单。接口通常包含多个方法,在每个装饰器中创建对它们的委托会生成大量样板代码。在 Kotlin 中情况不同--我们已经看到在 Kotlin 中类委托实际上是微不足道的。让我们看一些在装饰器模式中实际类委托使用的经典例子。假设我们想要将第一个位置作为元素添加到几个不同的ListAdapters中。这个额外的位置有一些特殊的属性。我们无法使用继承来实现这一点,因为这些不同列表的ListAdapters是不同类型的(这是标准情况)。在这种情况下,我们可以改变每个类的行为(DRY 规则),或者我们可以创建一个装饰器。这是这个装饰器的简短代码:

class ZeroElementListDecorator(val arrayAdapter: ListAdapter) : 
    ListAdapter by arrayAdapter { 
  override fun getCount(): Int = arrayAdapter.count + 1 
  override fun getItem(position: Int): Any? = when { 
      position == 0 -> null 
      else -> arrayAdapter.getItem(position - 1) 
  } 

  override fun getView(position: Int, convertView: View?,parent: 

ViewGroup): View = when { 
    position == 0 -> parent.context.inflator

        .inflate(R.layout.null_element_layout, parent, false) 
    else -> arrayAdapter.getView(position - 1, convertView, parent) 
  } 
} 

override fun getItemId(position: Int): Long = when { 
  position == 0 -> 0 
  else -> arrayAdapter.getItemId(position - 1) 
} 

我们在这里使用了Context的扩展属性inflator,这在 Kotlin Android 项目中经常包含,并且应该从第七章 扩展函数和属性中了解:

    val Context.inflater: LayoutInflater 
        get() = LayoutInflater.from(this) 

以这种方式定义的ZeroElementListDecorator类总是添加一个具有静态视图的第一个元素。在这里我们可以看到它的简单使用示例:

    val arrayList = findViewById(R.id.list) as ListView 
    val list = listOf("A", "B", "C") 
    val arrayAdapter = ArrayAdapter(this, 

          android.R.layout.simple_list_item_1, list) 
    arrayList.adapter = ZeroElementListDecorator(arrayAdapter) 

ZeroElementListDecorator中,我们可能会觉得需要重写四个方法很复杂,但实际上还有八个方法,我们不需要重写它们,这要归功于 Kotlin 的类委托。我们可以看到 Kotlin 类委托使得装饰器模式的实现变得更加容易。

装饰器模式实际上非常简单实现,而且非常直观。它可以在许多不同的情况下用来扩展类的额外功能。它非常安全,通常被称为一种良好的实践。这些例子只是类委托提供的可能性之一。我相信读者会发现更多使用这些模式的用例,并使用类委托使项目更加清晰、安全和简洁。

属性委托

Kotlin 不仅允许类委托,还允许属性委托。在本节中,我们将找出委托属性是什么,审查 Kotlin 标准库中的属性委托,并学习如何创建和使用自定义属性委托。

什么是委托属性?

让我们从解释什么是属性委托开始。这里是属性委托的使用示例:

    class User(val name: String, val surname: String) 

    var user: User by UserDelegate() // 1 

    println(user.name) 
    user = User("Marcin","Moskala")
  1. 我们将user属性委托给UserDelegate的一个实例(由构造函数创建)。

属性委托类似于类委托。我们使用相同的关键字(by)将属性委托给一个对象。对属性(set/get)的每次调用都将被委托给另一个对象(UserDelegate)。这样我们可以为多个属性重用相同的行为,例如,仅当满足某些条件时设置属性值,或者在访问/更新属性时添加日志条目。

我们知道属性实际上不需要后备字段。它可以只由 getter(只读)或 getter/setter(读/写)定义。在幕后,属性委托只是被转换为相应的方法调用(setValue/getValue)。上面的例子将被编译为这样的代码:

    var p$delegate = UserDelegate() 
    var user: User 
    get() = p$delegate.getValue(this, ::user) 
    set(value) { 
        p$delegate.setValue(this, ::user, value) 
    } 

该示例显示,通过使用by关键字,我们将 setter 和 getter 调用委托给委托。这就是为什么任何具有正确参数的getValuesetValue函数的对象(稍后将描述)都可以用作委托(对于只读属性,只需要getValue,因为只需要 getter)。重要的是,作为属性委托的所有类需要具有这两种方法。不需要接口。以下是UserDelegate的示例实现:

class UserDelegate { 
    operator fun getValue(thisRef: Any?, property: KProperty<*>): 

          User = readUserFromFile() 

    operator fun setValue(thisRef: Any?, property: KProperty<*>, 

          user:User) { 
        saveUserToFile(user) 
    } 
    //... 
} 

setValuegetValue方法用于设置和获取属性的值(属性设置器调用被委托给setValue方法,属性获取器将值委托给getValue方法)。这两个函数都需要标记为operator关键字。它们有一些特殊的参数集,用于确定委托可以服务的位置和属性。如果属性是只读的,那么对象只需要具有getValue方法就能够作为其委托:

class UserDelegate { 

    operator fun getValue(thisRef: Any?, property: KProperty<*>):

        User = readUserFromFile() 
} 

getValue方法返回的类型和用户在setValue方法中定义的属性的类型决定了委托属性的类型。

getValuesetValue函数的第一个参数(thisRef)的类型包含了委托使用的上下文的引用。它可以用于限制委托可以使用的类型。例如,我们可以以以下方式定义只能在Activity类内部使用的委托:

class UserDelegate { 

    operator fun getValue(thisRef: Activity, property: KProperty<*>): 

          User = thisRef.intent

          .getParcelableExtra("com.example.UserKey") 
} 

正如我们所见,所有上下文中都会提供对this的引用。只有在扩展函数或扩展属性中才会放置 null。对this的引用用于从上下文中获取一些数据。如果我们将其类型定义为Activity,那么我们只能在Activity内部(this的类型为Activity的任何上下文)中使用此委托。

此外,如果我们想要强制委托只能在顶层使用,我们可以将第一个参数(thisRef)的类型指定为Nothing?,因为这种类型的唯一可能值是null

这些方法中的另一个参数是property。它包含对委托属性的引用,其中包含其元数据(属性名称、类型等)。

属性委托可用于任何上下文中定义的属性(顶级属性、成员属性、局部变量等):

    var a by SomeDelegate() // 1 

    fun someTopLevelFun() { 
        var b by SomeDelegate() // 2 
    } 

    class SomeClass() { 
        var c by SomeDelegate() // 3 

        fun someMethod() { 
            val d by SomeDelegate() // 4 
        } 
    } 
  1. 使用委托的顶级属性

  2. 使用委托的局部变量(在顶级函数内部)

  3. 使用委托的成员属性

  4. 使用委托的局部变量(在方法内部)

在接下来的几节中,我们将描述 Kotlin 标准库中的委托。它们不仅因为它们经常有用而重要,而且因为它们是如何使用属性委托的好例子。

预定义的委托

Kotlin 标准库包含一些非常方便的属性委托。让我们讨论它们如何在实际项目中使用。

lazy函数

有时我们需要初始化一个对象,但我们希望确保对象只在第一次使用时初始化一次。在 Java 中,我们可以通过以下方式解决这个问题:

    private var _someProperty: SomeType? = null 
    private val somePropertyLock = Any() 
    val someProperty: SomeType 
    get() { 
        synchronized(somePropertyLock) { 
            if (_someProperty == null) { 
                _someProperty = SomeType() 
            } 
            return _someProperty!! 
        } 
    } 

这种构造在 Java 开发中很常见。Kotlin 允许我们通过提供lazy委托来以更简单的方式解决这个问题。它是最常用的委托。它只适用于只读属性(val),用法如下:

    val someProperty by lazy { SomeType() } 

标准库中提供委托的lazy函数:

    public fun <T> lazy(initializer: () -> T): 

          Lazy<T> =  SynchronizedLazyImpl(initializer) 

在这个例子中,SynchronizedLazyImpl 的对象被正式地用作属性委托。尽管通常它被称为惰性委托,来自于相应的函数名。其他委托也是从提供它们的函数的名称命名的。

惰性委托还具有线程安全机制。默认情况下,委托是完全线程安全的,但我们可以改变这种行为,使这个函数在我们知道永远不会有多个线程同时使用它的情况下更有效。要完全关闭线程安全机制,我们需要将enum类型值LazyThreadSafetyMode.NONE作为lazy函数的第一个参数。

val someProperty by lazy(LazyThreadSafetyMode.NONE) { SomeType() }

由于惰性委托,属性的初始化被延迟直到需要值。使用惰性委托提供了几个好处:

  • 更快的类初始化导致更快的应用程序启动时间,因为值的初始化被延迟到第一次使用它们时

  • 某些值可能永远不会在某些流程中使用,因此它们永远不会被初始化——我们在节省资源(内存、处理器时间、电池)。

另一个好处是有些对象需要在它们的类实例创建后才能创建。例如,在Activity中,我们不能在使用setContentView方法设置布局之前访问资源,这个方法通常在onCreate方法中调用。我将在这个例子中展示它。让我们看一下使用经典 Java 方式填充视图引用元素的 Java 类:

//Java 
public class MainActivity extends Activity { 

    TextView questionLabelView 
    EditText answerLabelView 
    Button confirmButtonView 

    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 

        questionLabelView = findViewById<TextView>

              (R.id.main_question_label);    
        answerLabelView   = findViewById<EditText>

              (R.id.main_answer_label);    
        confirmButtonView = findViewById<Button>

              (R.id.main_button_confirm);      
    } 
} 

如果我们将其翻译成 Kotlin,一对一,它将如下所示:

class MainActivity : Activity() { 

    var questionLabelView: TextView? = null 
    var answerLabelView: TextView? = null 
    var confirmButtonView: Button? = null 

    override fun onCreate(savedInstanceState: Bundle) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.main_activity) 

        questionLabelView = findViewById<TextView>

              (R.id.main_question_label)   

        answerLabelView = findViewById<TextView>

              (R.id.main_answer_label)

        confirmButtonView = findViewById<Button>

              (R.id.main_button_confirm)

    } 

}

使用惰性委托,我们可以以更简单的方式实现这种行为:

class MainActivity : Activity() { 

   val questionLabelView: TextView by lazy 

{ findViewById(R.id.main_question_label) as TextView } 
   val answerLabelView: TextView by lazy 

{ findViewById(R.id.main_answer_label) as TextView } 
   val confirmButtonView: Button by lazy 

{ findViewById(R.id.main_button_confirm) as Button } 

   override fun onCreate(savedInstanceState: Bundle) { 
     super.onCreate(savedInstanceState) 
     setContentView(R.layout.main_activity) 
   } 
} 

这种方法的好处如下:

  • 属性在一个地方声明和初始化,所以代码更简洁。

  • 属性是非空的,而不是可空的。这可以避免大量无用的空值检查。

  • 属性是只读的,因此我们可以获得所有的好处,比如线程同步或智能转换。

  • 传递给惰性委托的 lambda(包含findViewById)只有在第一次访问属性时才会执行。

  • 值将在类创建后被获取。这将加快启动速度。如果我们不使用其中一些视图,它们的值根本不会被获取(当视图复杂时,findViewById并不是一种高效的操作)。

  • 未使用的属性将被编译器标记。在 Java 实现中不会,因为编译器会注意到设置的值作为使用。

我们可以通过提取共同的行为并将其转换为扩展函数来改进前面的实现:

fun <T: View> Activity.bindView(viewId: Int) = lazy { findViewById(viewId) as T } 

然后,我们可以用更简洁的代码定义视图绑定:

class MainActivity : Activity() { 

  var questionLabelView: TextView by bindView(R.id.main_question_label)  // 1 
  var answerLabelView: TextView by bindView(R.id.main_answer_label)   // 1 
  var confirmButtonView: Button by bindView(R.id.main_button_confirm) // 1 

  override fun onCreate(savedInstanceState: Bundle) { 
    super.onCreate(savedInstanceState) 
    setContentView(R.layout.main_activity) 
  } 
} 
  1. 我们不需要为bindView函数提供的类型设置类型,因为它是从属性类型中推断出来的。

现在我们有一个单一的委托,在我们第一次访问特定视图时会在后台调用findViewById。这是一个非常简洁的解决方案。

还有另一种处理这个问题的方法。目前流行的是Kotlin Android Extension插件,它会在ActivitiesFragments中自动生成视图的自动绑定。我们将在第九章中讨论实际应用,制作你的 Marvel 画廊应用

即使有这样的支持,仍然有保持绑定的好处。一个是明确知道我们正在使用的视图元素,另一个是元素 ID 的名称和我们保存该元素的变量的名称之间的分离。此外,编译时间更快。

相同的机制可以应用于解决其他与 Android 相关的问题。例如,当我们向Activity传递参数时。标准的 Java 实现如下:

//Java 
class SettingsActivity extends Activity { 

  final Doctor DOCTOR_KEY = "doctorKey" 
  final String TITLE_KEY = "titleKey" 

  Doctor doctor 
  Address address 
  String title 

  public static void start ( Context context, Doctor doctor, 

  String title ) { 
    Intent intent = new Intent(context, SettingsActivity.class ) 
    intent.putExtra(DOCTOR_KEY, doctor) 
    intent.putExtra(TITLE_KEY, title) 
    context.startActivity(intent) 
  } 

  @Override 
  public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_main); 

    doctor = getExtras().getParcelable(DOCTOR_KEY)   
    title = getExtras().getString(TITLE_KEY)   

    ToastHelper.toast(this, doctor.id) 
    ToastHelper.toast(this, title) 
  } 
} 

我们可以在 Kotlin 中编写相同的实现,但也可以在变量声明时检索参数值(getString / getParcerable )。为此,我们需要以下扩展函数:

fun <T : Parcelable> Activity.extra(key: String) = lazy 

    { intent.extras.getParcelable<T>(key) } 

fun Activity.extraString(key: String) = lazy 

    { intent.extras.getString(key) } 

然后我们可以通过使用 extraextraString 委托来获取额外的参数:

class SettingsActivity : Activity() { 

    private val doctor by extra<Doctor>(DOCTOR_KEY) // 1 
    private val title by extraString(TITLE_KEY) // 1 

    override fun onCreate(savedInstanceState: Bundle?) { 
        super.onCreate(savedInstanceState) 
        setContentView(R.layout.settings_activity) 
        toast(doctor.id) // 2 
        toast(title) // 2 
    } 

    companion object { // 3 
        const val DOCTOR_KEY = "doctorKey" 
        const val TITLE_KEY = "titleKey" 

    fun start(context: Context, doctor: Doctor, title: String) { // 3 
        ontext.startActivity(getIntent<SettingsActivity>().apply { // 4 
            putExtra(DOCTOR_KEY, doctor) // 5 
            putExtra(TITLE_KEY, title) // 5 
        }) 
    } 
  } 

} 
  1. 我们正在定义应该从 Activity 参数中检索值的属性,使用相应的键。

  2. onCreate 方法中,我们从参数中访问属性。当我们请求属性(使用 getter)时,延迟委托将从额外中获取其值,并将其存储以供以后使用。

  3. 要创建一个启动活动的静态方法,我们需要使用伴生对象。

  4. SettingsActivity::class.java 是 Java 类引用 SettingsActivity.class 的类似物。

  5. 我们正在使用第七章中定义的方法,扩展函数和属性

我们还可以编写函数来检索其他可以由 Bundle 持有的类型(例如 LongSerializable )。这是一个非常好的替代方案,可以避免使用诸如 ActivityStarter 等参数注入库,从而保持非常快的编译时间。我们可以使用类似的函数来绑定字符串、颜色、服务、存储库和模型和逻辑的其他部分:

fun <T> Activity.bindString(@IdRes id: Int): Lazy<T> = 

    lazy { getString(id) } 
fun <T> Activity.bindColour(@IdRes id: Int): Lazy<T> = 

    lazy { getColour(id) } 

Activity 中,所有繁重的或依赖于参数的内容都应该使用延迟委托(或异步提供)。同时,所有依赖于需要延迟初始化的元素的元素也应该定义为延迟。例如,依赖于 doctor 属性的 presenter 的定义:

    val presenter by lazy { MainPresenter(this, doctor) } 

否则,尝试构造 MainPresenter 对象将在类创建时进行,此时我们还不能从意图中读取值,也无法填充 doctor 属性,应用程序将崩溃。

我认为这些示例足以让我们相信,延迟委托在 Android 项目中非常有用。它也是一个很好的属性委托入门,因为它简单而优雅。

notNull 函数

notNull 委托是最简单的标准库委托,这就是为什么它将首先被介绍。使用方法如下:

    var someProperty: SomeType by notNull()

提供大多数标准库委托(包括 notNull 函数)的函数是在 object 委托中定义的。要使用它们,我们需要引用这个对象(Delegates.notNull() ),或者导入它(import kotlin.properties.Delegates.notNull )。在示例中,我们将假设这个 object 已经被导入,因此我们将省略对它的引用。

notNull 委托允许我们将变量定义为非空,即在稍后初始化而不是在对象构造时初始化。我们可以定义变量为非空而不提供默认值。notNull 函数是 lateinit 的一种替代方式:

    lateinit var someProperty: SomeType 

notNull 委托提供了几乎与 lateinit 相同的效果(只是错误消息不同)。在尝试在设置值之前使用此属性时,它将抛出 IllegalStateException 并终止 Android 应用程序。因此,只有在我们知道值将在第一次尝试使用之前设置时,才应该使用它。

lateinitnotNull 委托之间的区别非常简单。lateinitnotNull 委托更快,因此应尽可能使用 lateinit 委托。但它有限制,lateinit 不能用于原始类型或顶级属性,因此在这种情况下,应使用 notNull 代替。

让我们来看一下 notNull 委托的实现。以下是 notNull 函数的实现:

    public fun <T: Any> notNull(): ReadWriteProperty<Any?, T> =  

        NotNullVar() 

如我们所见,notNull 实际上是一个返回对象的函数,该对象是我们实际委托的实例,隐藏在 ReadWriteProperty 接口后面。让我们来看一个实际的委托定义:

private class NotNullVar<T: Any>() : ReadWriteProperty<Any?, T> { // 1 
  private var value: T? = null 

  public override fun getValue(thisRef: Any?, 

  property: KProperty<*>): T { 
     return value ?: throw IllegalStateException("Property 

            ${property.name} should be initialized before get.") // 2 
  } 

  public override fun setValue(thisRef: Any?, 

  property: KProperty<*>, value: T) { 
     this.value = value 
  } 
} 
  1. 类是私有的。这是可能的,因为它是由函数 notNull 提供的,该函数将其作为 ReadWriteProperty<Any?, T> 返回,而该接口是公共的。

  2. 这里展示了如何提供返回值。如果在使用过程中为 null,则表示未设置值,方法将抛出错误。否则,它会返回该值。

这个委托应该很容易理解。setValue函数将值设置为可空字段,getValue如果不为 null 则返回该字段,如果为 null 则抛出异常。以下是此错误的示例:

    var name: String by Delegates.notNull() 
    println(name) 

    // Error: Property name should be initialized before get. 

这是一个关于委托属性使用的非常简单的例子,也是对属性委托工作原理的良好介绍。委托属性是非常强大的构造,具有多种应用。

可观察委托

可观察是可变属性最有用的标准库委托。每次设置一个值(调用setValue方法)时,都会调用声明中的 lambda 函数。可观察委托的一个简单示例如下:

    var name: String by Delegates.observable("Empty"){ 
        property, oldValue, newValue -> // 1 
        println("$oldValue -> $newValue") // 2 
    } 

    // Usage 
    name = "Martin" // 3, 

    Prints: Empty -> Martin 
    name = "Igor" // 3, 

    Prints: Martin -> Igor 
    name = "Igor" // 3, 4 

    Prints: Igor -> Igor
  1. lambda 函数的参数如下:
  • property:委托属性的引用。这里是对 name 的引用。这与setValuegetValue中描述的属性相同。它是KProperty类型。在这种情况下(以及大多数情况下),当未使用时可以使用下划线(“_”符号)代替。

  • oldValue:更改前的property的先前值。

  • newValue:更改后的property的新值。

  1. 每次将新值设置到属性时都会调用 lambda 函数。

  2. 当我们设置新值时,该值会更新,但同时也会调用委托中声明的 lambda 方法。

  3. 注意,每次使用 setter 时都会调用 lambda,并且不管新值是否等于先前的值都没有关系。

特别重要的是要记住,每次设置新值时都会调用 lambda,而不是在对象的内部状态更改时。例如:

    var list: MutableList<Int> by observable(mutableListOf()) 

    { _, old, new ->  
        println("List changed from $old to $new") 
    } 

    // Usage 
    list.add(1)  // 1 
    list =  mutableListOf(2, 3) 

    // 2, prints: List changed from [1] to [2, 3] 
  1. 不打印任何内容,因为我们没有更改属性(未使用 setter)。我们只更改了列表内部定义的属性,而不是对象本身。

  2. 在这里我们改变了列表的值,因此会调用可观察委托中的 lambda 函数并打印文本。

可观察委托对于不可变类型非常有用,与可变类型相反。幸运的是,Kotlin 中的所有基本类型默认都是不可变的(ListMapSetIntString)。让我们看一个实际的 Android 示例:

    class SomeActivity : Activity() { 

        var list: List<String> by Delegates.observable(emptyList()) { 
            prop, old, new -> if(old != new) updateListView(new) 
        }   
        //  ... 
    } 

每次更改列表时,视图都会更新。请注意,虽然List是不可变的,但是当我们想要应用任何更改时,我们需要使用 setter,以便确保在此操作之后列表将被更新。这比记住每次列表更改时都调用updateListView方法要容易得多。这种模式可以广泛用于项目中声明编辑视图的属性。它改变了更新视图机制的工作方式。

使用可观察委托可以解决的另一个问题是,在ListAdapters中,列表中的元素每次更改时都必须调用notifyDataSetChanged。在 Java 中,经典解决方案是封装此列表,并在修改它的每个函数中调用notifyDataSetChanged。在 Kotlin 中,我们可以使用可观察属性委托来简化这个过程:

var list: List<LocalDate> by observable(list) { _, old, new ->  // 1 
  if(new != old) notifyDataSetChanged() 
} 
  1. 请注意,这里的列表是不可变的,因此没有办法在不使用notifyDataSetChanged的情况下更改其元素。

可观察委托用于定义在属性值更改时应发生的行为。当我们有应该在每次更改属性时执行的操作,或者当我们想要将属性值与视图或其他值绑定时,它最常用。但在函数内部,我们无法决定是否设置新值。为此,可以使用vetoable委托。

可否决的委托

vetoable函数是一个标准库属性委托,其工作方式类似于可观察委托,但有两个主要区别:

  • 在设置新值之前,会先调用参数中的 lambda

  • 它允许声明中的 lambda 函数决定是否接受或拒绝新值

例如,如果我们假设列表必须始终包含比旧列表更多的项目,则我们将定义以下vetoable委托:

var list: List<String> by Delegates.vetoable(emptyList()) 

{ _, old, new ->  
   new.size > old.size 
} 

如果新列表不包含比旧列表更多的项目,则值将不会更改。因此,我们可以将vetoable视为observable,它也决定是否应更改值。假设我们想要将列表绑定到视图,但它至少需要有三个元素。我们不允许进行任何可能导致其具有更少元素的更改。实现如下:

var list: List<String> by Delegates.vetoable(emptyList()) 

{ prop, old, new ->  
    if(new.size < 3) return@vetoable false // 1 
    updateListView(new) 
    true // 2 
} 
  1. 如果新列表的大小小于 3,则我们不接受它,并从 lambda 返回false。通过标签返回的false值(用于从 lambda 表达式返回)是新值不应被接受的信息。

  2. 此 lambda 函数需要返回一个值。此值可以从带有标签的return中获取,也可以从 lambda 主体的最后一行获取。这里的值true表示应接受新值。

这是其用法的一个简单示例:

    listVetoable = listOf("A", "B", "C") // Update A, B, C 
    println(listVetoable) // Prints: [A, B, C] 
    listVetoable = listOf("A") // Nothing happens 
    println(listVetoable) // Prints: [A, B, C] 
    listVetoable = listOf("A", "B", "C", "D", "E")  

    // Prints: [A, B, C, D, E] 

由于某些其他原因,我们还可以使其不可改变,例如,我们可能仍在加载数据。此外,可否决的属性委托可以用于验证器。例如:

    var name: String by Delegates.vetoable("") 

    { prop, old, new ->  
    if (isValid(new)) { 
        showNewData(new) 
        true 
    } else { 
        showNameError() 
        false 
    }

此属性只能更改为符合谓词isValid(new)的值。

将属性委托给 Map 类型

标准库包含了对具有String键类型的MapMutableMap的扩展,提供了getValuesetValue函数。由于它们,map也可以用作属性委托:

    class User(map: Map<String, Any>) { // 1 
        val name: String by map 
        val kotlinProgrammer: Boolean by map 
    } 

    // Usage 
    val map: Map<String, Any> = mapOf( // 2 
        "name" to "Marcin", 
        "kotlinProgrammer" to true 
    ) 
    val user = User(map) // 3 
    println(user.name)  // Prints: Marcin 
    println(user.kotlinProgrammer)  // Prints: true 
  1. 映射键类型需要是String,而值类型没有限制。通常是AnyAny?

  2. 创建包含所有值的Map

  3. 为对象提供一个map

当我们在Map中保存数据时,这可能很有用,也适用于以下情况:

  • 当我们想要简化对这些值的访问时

  • 当我们定义一个结构,告诉我们应该在此映射中期望哪种键

  • 当我们要求委托给Map的属性时,其值将从此映射值中获取,键等于属性名称

它是如何实现的?这是标准库中的简化代码:

operator fun <V, V1: V> Map<String, V>.getValue( // 1 
      thisRef: Any?, // 2 
      property: KProperty<*>): V1 { // 3 
          val key = property.name // 4 
          val value = get(key) 
          if (value == null && !containsKey(key)) { 
              throw NoSuchElementException("Key ${property.name} 

              is missing in the map.") 
          } else { 
              return value as V1 // 3 
          } 
      } 
  1. V 是列表上的一种值

  2. thisRef的类型是Any?,因此Map可以在任何上下文中用作属性委托。

  3. V1是返回类型。这通常是从属性推断出来的,但它必须是类型V的子类型

  4. 属性的名称用作map上的key

请记住,这只是一个扩展函数。对象要成为委托所需的一切就是包含getValue方法(对于读写属性还需要setValue)。我们甚至可以使用object声明从匿名类的对象创建委托:

val someProperty by object { // 1 
    operator fun  getValue(thisRef: Any?, 

    property: KProperty<*>) = "Something" 
} 
println(someProperty) // prints: Something 
  1. 对象没有实现任何接口。它只包含具有正确签名的getValue方法。这足以使其作为只读属性委托工作。

请注意,在请求属性的值时,map中需要有一个具有这样名称的条目,否则将抛出错误(使属性可为空不会改变它)。

将字段委托给 map 可能很有用,例如,当我们从 API 中获得一个具有动态字段的对象时。我们希望将提供的数据视为对象,以便更轻松地访问其字段,但我们还需要将其保留为映射,以便能够列出 API 提供的所有字段(甚至是我们没有预期的字段)。

在前面的示例中,我们使用了不可变的Map;因此,对象属性是只读的(val)。如果我们想要创建一个可以更改的对象,那么我们应该使用MutableMap,然后可以将属性定义为可变的(var)。这是一个例子:

class User(val map: MutableMap<String, Any>) { 
    var name: String by map 
    var kotlinProgrammer: Boolean by map 

    override fun toString(): String = "Name: $name, 

    Kotlin programmer: $kotlinProgrammer" 
} 

// Usage 
val map = mutableMapOf( // 1 
    "name" to "Marcin", 
    "kotlinProgrammer" to true 
) 
val user = User(map) 
println(user) // prints: Name: Marcin, Kotlin programmer: true 
user.map.put("name", "Igor") // 1  
println(user) // prints: Name: Igor, Kotlin programmer: true 
user.name = "Michal" // 2 
println(user) // prints: Name: Michal, Kotlin programmer: true 
  1. 属性值可以通过更改map的值来更改

  2. 属性值也可以像其他属性一样更改。真正发生的是值的更改被委托给setValue,它正在更改map

虽然这里的属性是可变的,但setValue函数也必须提供。它被实现为MutableMap的扩展函数。以下是简化的代码:

    operator fun <V> MutableMap<String, V>.setValue( 
        thisRef: Any?,  
        property: KProperty<*>,  
        value: V 
    ) { 
        put(property.name, value) 
    } 

请注意,即使是如此简单的函数也可以允许使用常见对象的创新方式。这显示了属性委托所提供的可能性。

Kotlin 允许我们定义自定义委托。现在,我们可以找到许多库,提供了可以用于 Android 中不同目的的新属性委托。在 Android 中可以使用属性委托的各种方式。在下一节中,我们将看到一些自定义属性委托的例子,并且我们将看看这个功能在哪些情况下真的很有帮助。

自定义委托

以前的所有委托都来自标准库,但我们可以轻松实现自己的属性委托。我们已经看到,为了允许一个类成为委托,我们需要提供getValuesetValue函数。它们必须具有具体的签名,但无需扩展类或实现接口。要将对象用作委托,我们甚至不需要更改其内部实现,因为我们可以将getValuesetValue定义为扩展函数。但是,当我们创建自定义类以成为委托时,接口可能会有用:

  • 它将定义函数结构,这样我们就可以在 Android Studio 中生成适当的方法。

  • 如果我们正在创建库,那么我们可能希望将委托类设置为私有或内部,以防止不当使用。我们在notNull部分看到了这种情况,其中类NotNullVar是私有的,并且作为ReadWriteProperty<Any?, T>的接口。

提供完整功能以允许某个类成为委托的接口是ReadOnlyProperty(用于只读属性)和ReadWriteProperty(用于读写属性)。这些接口非常有用,让我们看看它们的定义:

    public interface ReadOnlyProperty<in R, out T> { 
        public operator fun getValue(thisRef: R, 

            property: KProperty<*>): T 
    } 

    public interface ReadWriteProperty<in R, T> { 
       public operator fun getValue(thisRef: R, 

           property: KProperty<*>): T 
       public operator fun setValue(thisRef: R, 

           property: KProperty<*>, value: T) 
    } 

参数的值已经解释过了,但让我们再看一遍:

  • thisRef:委托使用的对象的引用。其类型定义了委托可以使用的上下文。

  • property:包含有关委托属性的数据的引用。它包含有关此属性的所有信息,例如其名称或类型。

  • value:要设置的新值。

thisRefproperty参数在以下委托中未使用:Lazy、Observable 和 Vetoable。MapMutableMapnotNull使用属性来获取键的属性名称。但是这些参数可以在不同的情况下使用。

让我们看一些小而有用的自定义属性委托的例子。我们已经看到了用于只读属性的延迟属性委托;然而,有时我们需要一个可变的延迟属性。如果在初始化之前要求值,那么它应该从初始化程序中填充其值并返回它。在其他情况下,它应该像普通的可变属性一样工作:

fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T> = MutableLazy<T>(initializer) 

private class MutableLazy<T>(val initializer: () -> T) : ReadWriteProperty<Any?, T> { 

   private var value: T? = null 
   private var initialized = false 

   override fun getValue(thisRef: Any?, property: KProperty<*>): T { 
       synchronized(this) { 
           if (!initialized) { 
               value = initializer() 
           } 
           return value as T 
       } 
   } 

   override fun setValue(thisRef: Any?, 

       property: KProperty<*>, value: T) { 
       synchronized(this) { 
           this.value = value 
           initialized = true 
       } 
   } 
} 
  1. 委托被隐藏在接口后面,并由一个函数提供,因此允许我们更改MutableLazy的实现,而不必担心它会影响使用它的代码。

  2. 我们正在实现ReadWriteProperty。这是可选的,但非常有用,因为它强制了读写属性的正确结构。它的第一个类型是Any?,意味着我们可以在任何上下文中使用这个属性委托,包括顶层。它的第二个类型是泛型。请注意,对这种类型没有限制,因此它也可能是可空的。

  3. 属性的值存储在value属性中,其存在性存储在一个初始化的属性中。我们需要这样做是因为我们希望允许T是可空类型。然后值中的null可能意味着它尚未初始化,或者它只是等于null

  4. 我们不需要使用operator修饰符,因为它已经在接口中使用了。

  5. 如果在设置任何值之前调用getValue,则该值将使用初始化程序填充。

  6. 我们需要将值转换为T,因为它可能不为空,并且我们将值初始化为可空,初始值为 null。

这种属性委托在 Android 开发中的不同用例中可能会很有用;例如,当属性的默认值存储在文件中,我们需要读取它(这是一个繁重的操作):

    var gameMode : GameMode by MutableLazy { 
        getDefaultGameMode()  
    } 

    var mapConfiguration : MapConfiguration by MutableLazy { 
        getSavedMapConfiguration() 
    } 

    var screenResolution : ScreenResolution by MutableLazy { 
        getOptimalScreenResolutionForDevice() 
    } 

这样,如果用户在使用之前设置了此属性的自定义值,我们就不必自己计算它。第二个自定义属性委托将允许我们定义属性的 getter:

    val a: Int get() = 1 
    val b: String get() = "KOKO" 
    val c: Int get() = 1 + 100 

在 Kotlin 1.1 之前,我们总是需要定义属性的类型。为了避免这种情况,我们可以定义以下扩展函数到函数类型(因此也是 lambda 表达式):

    inline operator fun <R> (() -> R).getValue( 
        thisRef: Any?, 
        property: KProperty<*> 
    ): R = invoke() 

然后我们可以这样定义具有类似行为的属性:

    val a by { 1 } 
    val b by { "KOKO" } 
    val c by { 1 + 100 } 

这种方式不被推荐,因为它的效率降低,但它是委托属性提供给我们的可能性的一个很好的例子。这样一个小的扩展函数将函数类型转换为属性委托。这是在 Kotlin 编译后的简化代码(请注意,扩展函数被标记为内联,因此它的调用被替换为它的主体):

    private val `a$delegate` = { 1 } 
    val a: Int get() = `a$delegate`() 
    private val `b$delegate` = {  "KOKO" } 
    val b: String get() = `b$delegate`() 
    private val `c$delegate` = { 1 + 100 } 
    val c: Int get() = `c$delegate`() 

在下一节中,我们将看到为真实项目创建的一些自定义委托。它们将与它们解决的问题一起呈现。

视图绑定

当我们在项目中使用Model-View-PresenterMVP)时,我们需要通过 Presenter 在 View 中进行所有更改。因此,我们被迫在视图上创建多个函数,例如:

    override fun getName(): String { 
        return nameView.text.toString() 
    } 

    override fun setName(name: String) { 
        nameView.text = name 
    } 

我们还必须在以下interface中定义函数:

    interface MainView { 
        fun getName(): String 
        fun setName(name: String) 
    } 

通过使用属性绑定,我们可以简化前面的代码并减少对 setter/getter 方法的需求。我们可以将属性绑定到视图元素。这是我们想要实现的结果:

    override var name: String by bindToTex(R.id.textView) 

interface

    interface MainView { 
        var name: String 
    } 

前面的例子更简洁,更易于维护。请注意,我们通过参数提供元素 ID。一个简单的类将给我们带来预期的结果,如下所示:

fun Activity.bindToText( 
    @IdRes viewId: Int ) = object : 

    ReadWriteProperty<Any?, String> { 

  val textView by lazy { findViewById<TextView>(viewId) } 

  override fun getValue(thisRef: Any?, 

      property: KProperty<*>): String { 
      return textView.text.toString() 
  } 

  override fun setValue(thisRef: Any?, 

      property: KProperty<*>, value: String) { 
      textView.text = value 
  } 
} 

我们可以为不同的视图属性和不同的上下文(FragmentService)创建类似的绑定。另一个非常有用的工具是绑定到可见性,它将逻辑属性(类型为Boolean)绑定到view元素的可见性:

fun Activity.bindToVisibility( 
   @IdRes viewId: Int ) = object : 

   ReadWriteProperty<Any?, Boolean> { 

   val view by lazy { findViewById(viewId) } 

  override fun getValue(thisRef: Any?, 

      property: KProperty<*>): Boolean { 
      return view.visibility == View.VISIBLE 
  } 

  override fun setValue(thisRef: Any?, 

      property: KProperty<*>, value: Boolean) { 
      view.visibility = if(value) View.VISIBLE else View.GONE 
  } 
} 

这些实现提供了在 Java 中很难实现的可能性。类似的绑定可以用于其他View元素,以使 MVP 的使用更简洁和简单。刚刚呈现的片段只是简单的例子,但更好的实现可以在库KotlinAndroidViewBindings中找到(github.com/MarcinMoskala/KotlinAndroidViewBindings)。

首选绑定

为了展示更复杂的例子,我们将尝试帮助使用SharedPreferences。对于这个问题,有更好的 Kotlin 方法,但这个尝试很好分析,并且是我们在扩展属性上使用属性委托的一个合理例子。因此,我们希望能够将保存在SharedPreferences中的值视为SharedPreferences对象的属性。以下是示例用法:

    preferences.canEatPie = true 
    if(preferences.canEatPie) { 
        // Code 
    } 

如果我们定义以下扩展属性定义,我们就可以实现它:

    var SharedPreferences.canEatPie: 

    Boolean by bindToPreferenceField(true) // 1

    var SharedPreferences.allPieInTheWorld: 

    Long by bindToPreferenceField(0,"AllPieKey") //2
  1. 布尔类型的属性。当属性是非空时,必须在函数的第一个参数中提供默认值。

  2. 属性可以提供自定义键。这在实际项目中非常有用,因为我们必须控制这个键(例如,不要在属性重命名时无意中更改它)。

让我们通过深入研究非空属性的工作原理来分析它是如何工作的。首先,让我们看看提供函数。请注意,属性的类型决定了从 SharedPreferences 中获取值的方式(因为有不同的函数,比如 getStringgetInt 等)。为了获取它,我们需要将这个类类型作为 inline 函数的 reified 类型提供,或者通过参数提供。这就是委托提供函数的样子:

inline fun <reified T : Any> bindToPreferenceField( 
      default: T?, 
      key: String? = null 
): ReadWriteProperty<SharedPreferences, T> // 1 
    = bindToPreferenceField(T::class, default, key) 

fun <T : Any> bindToPreferenceField( // 2 
    clazz: KClass<T>, 
    default: T?, 
    key: String? = null 
): ReadWriteProperty<SharedPreferences, T> 
      = PreferenceFieldBinder(clazz, default, key) // 1 
  1. 这两个函数都返回接口 ReadWriteProperty<SharedPreferences, T> 后面的对象。请注意,这里的上下文设置为 SharedPreferences,因此只能在那里或在 SharedPreferences 扩展中使用。定义这个函数是因为类型参数不能重新定义,我们需要将类型作为普通参数提供。

  2. 请注意,bindToPreferenceField 函数不能是私有的或内部的,因为内联函数只能使用相同或更少限制的函数。

最后,让我们看看 PreferenceFieldDelegate 类,它是我们的委托:

internal open class PreferenceFieldDelegate<T : Any>( 
      private val clazz: KClass<T>, 
      private val default: T?, 
      private val key: String? 
) : ReadWriteProperty<SharedPreferences, T> { 

  override operator fun getValue(thisRef: SharedPreferences, 

  property: KProperty<*>): T

    = thisRef.getLong(getValue<T>(clazz, default, getKey(property))

  override fun setValue(thisRef: SharedPreferences, 

  property: KProperty<*>, value: T) { 
     thisRef.edit().apply 

     { putValue(clazz, value, getKey(property)) }.apply() 
  } 

  private fun getKey(property: KProperty<*>) = 

  key ?: "${property.name}Key" 
} 

现在我们知道了 thisRef 参数的用法。它的类型是 SharedPreferences,我们可以使用它来获取和设置所有的值。以下是用于根据属性类型获取和保存值的函数的定义:

internal fun SharedPreferences.Editor.putValue(clazz: KClass<*>, value: Any, key: String) {

   when (clazz.simpleName) {

       "Long" -> putLong(key, value as Long)

       "Int" -> putInt(key, value as Int)

       "String" -> putString(key, value as String?)

       "Boolean" -> putBoolean(key, value as Boolean)

       "Float" -> putFloat(key, value as Float)

       else -> putString(key, value.toJson())

   }

}

internal fun <T: Any> SharedPreferences.getValue(clazz: KClass<*>, default: T?, key: String): T = when (clazz.simpleName) {

   "Long" -> getLong(key, default as Long)

   "Int" -> getInt(key, default as Int)

   "String" -> getString(key, default as? String)

   "Boolean" -> getBoolean(key, default as Boolean)

   "Float" -> getFloat(key, default as Float)

   else -> getString(key, default?.toJson()).fromJson(clazz)

} as T

我们还需要定义 toJsonfromJson

var preferencesGson: Gson = GsonBuilder().create()

internal fun Any.toJson() = preferencesGson.toJson(this)!!

internal fun <T : Any> String.fromJson(clazz: KClass<T>) = preferencesGson.fromJson(this, clazz.java)

有了这样的定义,我们可以为 SharedPreferences 定义额外的扩展属性:

var SharedPreferences.canEatPie: Boolean by bindToPreferenceField(true) 

正如我们在第七章 扩展函数和属性 中已经看到的,Java 中没有我们可以添加到类中的字段。在底层,扩展属性被编译为 getter 和 setter 函数,并且它们将调用委托创建。

val 'canEatPie$delegate' = bindToPreferenceField(Boolean::class, true) 

fun SharedPreferences.getCanEatPie(): Boolean { 
  return 'canEatPie$delegate'.getValue(this, 

  SharedPreferences::canEatPie) 
} 

fun SharedPreferences.setCanEatPie(value: Boolean) { 
  'canEatPie$delegate'.setValue(this, SharedPreferences::canEatPie, 

   value) 
} 

还要记住,扩展函数实际上只是带有第一个参数扩展的静态函数:

val 'canEatPie$delegate' = bindToPreferenceField(Boolean::class, true) 

fun getCanEatPie(receiver: SharedPreferences): Boolean {

   return 'canEatPie$delegate'.getValue(receiver, 

   SharedPreferences::canEatPie)

}

fun setCanEatPie(receiver: SharedPreferences, value: Boolean) {

   'canEatPie$delegate'.setValue(receiver, 

    SharedPreferences::canEatPie, value)

}

介绍的例子应该足以理解属性委托的工作原理以及它们的用法。属性委托在 Kotlin 开源库中被广泛使用。它们被用于快速简单的依赖注入(例如 Kodein、Injekt、TornadoFX)、绑定到视图、SharedPreferences 或其他元素(已经包括 PreferenceHolderKotlinAndroidViewBindings)、在配置定义中定义属性键(例如 Konfig),甚至用于定义数据库列结构(例如 Kwery)。还有许多用法等待被发现。

提供委托

自 Kotlin 1.1 开始,有一个名为 provideDelegate 的操作符,用于在类初始化期间提供委托。provideDelegate 的主要动机是它允许根据属性的特性(名称、类型、注解等)提供自定义委托。

provideDelegate 操作符返回委托,所有具有此操作符的类型不需要自己是委托就可以作为委托使用。以下是一个例子:

    class A(val i: Int) { 

        operator fun provideDelegate( 
            thisRef: Any?, 
            prop: KProperty<*> 
        ) = object: ReadOnlyProperty<Any?, Int> { 

            override fun getValue( 
                thisRef: Any?, 
                property: KProperty<*> 
            ) = i 
        } 
    } 

    val a by A(1) 

在这个例子中,A 被用作委托,虽然它既不实现 getvalue 也不实现 setvalue 函数。这是可能的,因为它定义了一个 provideDelegate 操作符,它返回将用于代替 A 的委托。属性委托被编译为以下代码:

    private val a$delegate = A().provideDelegate(this, this::prop) 
    val a: Int 
    get() = a1$delegate.getValue(this, this::prop) 

在 Kotlin 支持的库 ActivityStarter 的一部分中可以找到实际的例子(github.com/MarcinMoskala/ActivityStarter)。活动参数是使用注解定义的,但我们可以使用属性委托来简化从 Kotlin 使用,并允许属性定义为可能是只读的而不是 lateinit

    @get:Arg(optional = true) val name: String by argExtra(defaultName)

    @get:Arg(optional = true) val id: Int by argExtra(defaultId)

    @get:Arg val grade: Char  by argExtra()

    @get:Arg val passing: Boolean  by argExtra() 

但也有一些要求:

  • 当使用 argExtra 时,属性的 getter 必须被注解

  • 如果参数是可选的,并且类型不可为空,我们需要指定默认值。

为了检查这些要求,我们需要引用属性以获取 getter 注释。我们不能在 argExtra 函数中拥有这样的引用,但我们可以在 provideDevegate 中实现它们:

fun <T> Activity.argExtra(default: T? = null) = ArgValueDelegateProvider(default)

fun <T> Fragment.argExtra(default: T? = null) = ArgValueDelegateProvider(default)

fun <T> android.support.v4.app.Fragment.argExtra(default: T? = null) = 

        ValueDelegateProvider(default)

class ArgValueDelegateProvider<T>(val default: T? = null) {

    operator fun provideDelegate(

        thisRef: Any?,

        prop: KProperty<*>

    ): ReadWriteProperty<Any, T> {

        val annotation = prop.getter.findAnnotation<Arg>()

        when {

            annotation == null -> 

            throw Error(ErrorMessages.noAnnotation)

            annotation.optional && !prop.returnType.isMarkedNullable && 

            default == null -> 

            throw Error(ErrorMessages.optionalValueNeeded)

        }

        return ArgValueDelegate(default)

    }

}

internal object ErrorMessages {

    const val noAnnotation = 

     "Element getter must be annotated with Arg"

    const val optionalValueNeeded = 

    "Arguments that are optional and have not-

        nullable type must have defaut value specified"

}

当条件不满足时,这种委托会抛出适当的错误:

val a: A? by ArgValueDelegateProvider() 

// Throws error during initialization: Element getter must be annotated with Arg

@get:Arg(optional = true) val a: A by ArgValueDelegateProvider() 在初始化期间抛出错误:必须指定可选且非空类型的参数的默认值

这种方式在对象初始化期间,不接受不可接受的参数定义,而是抛出适当的错误,而不是在意外情况下破坏应用程序。

总结

在本章中,我们描述了类委托、属性委托,以及它们如何用于消除代码中的冗余。我们将委托定义为其他对象或属性调用的对象。我们学习了与类委托密切相关的委托模式和装饰器模式的设计模式。

委托模式被提及为继承的一种替代方案,装饰器模式是一种向实现相同接口的不同类添加功能的方式。我们已经看到了属性委托的工作原理,以及 Kotlin 标准库的属性委托:notNulllazyobservablevetoable,以及使用 Map 作为委托的用法。我们学习了它们的工作原理以及何时应该使用它们。我们还看到了如何制作自定义属性委托,以及实际用例示例。

对不同特性及其用法的了解是不够的,还需要理解它们如何结合在一起构建出色的应用程序。在下一章中,我们将编写一个演示应用程序,并解释本书中描述的各种 Kotlin 特性如何结合在一起。

第九章:制作您的 Marvel 画廊应用程序

我们已经看到了最重要的 Kotlin 功能,它们使得 Android 开发更加简单和高效,但仅仅通过查看这些部分很难理解整个画面。因此,在本章中,我们将构建一个完整的用 Kotlin 编写的 Android 应用程序。

在本章中,选择要实现的应用程序是一个艰难的决定。它必须简短而简单,但同时应尽可能多地利用 Kotlin 功能。同时,我们希望最小化使用的库的数量,因为这是一本关于 Kotlin 的 Android 开发书籍,而不是关于 Android 库的书籍。我们希望它看起来尽可能好,但同时我们也希望避免实现自定义图形元素,因为它们通常复杂且实际上并不从 Kotlin 的角度提供好处。

我们最终决定制作一个 Marvel 画廊应用程序--一个小型应用程序,我们可以用来查找我们最喜欢的 Marvel 角色并显示他们的详细信息。所有数据都是通过 Marvel 网站的 API 提供的。

Marvel 画廊

让我们实现我们的 Marvel 画廊应用程序。该应用程序应允许以下用例:

  • 启动应用程序后,用户可以看到一个角色画廊。

  • 启动应用程序后,用户可以通过角色名称搜索角色。

  • 当用户点击角色图片时,会显示一个简介。角色简介包括角色名称、照片、描述和出现次数。

以下是描述应用程序主要功能的三种用例。在接下来的章节中,我们将逐一实现它们。如果在本章中迷失了方向,记住您可以随时在 GitHub 上查看完整的应用程序(github.com/MarcinMoskala/MarvelGallery)。

为了更好地理解我们想要构建的内容,让我们看一些来自我们应用程序最终版本的截图:

如何使用本章

本章展示了构建应用程序所需的所有步骤和代码。其目的是展示应用程序开发的逐步过程。在阅读本章时,专注于开发过程,并尝试理解所呈现的代码的目的。您不需要完全理解布局,也不必理解单元测试的定义,只要理解它们在做什么即可。专注于应用程序结构和使最终代码更简单的 Kotlin 解决方案。大多数解决方案已在前几章中进行了描述,因此只有简要描述。本章的价值在于它们的使用是在具体应用程序的上下文中呈现的。

您可以从 GitHub(github.com/MarcinMoskala/MarvelGallery)下载应用程序代码。

在 GitHub 上,您可以查看最终代码,下载它,或者使用 Git 将其克隆到您的计算机上:

git clone git@github.com:MarcinMoskala/MarvelGallery.git

该应用程序还包括使用Espresso编写的 UI 测试,但本章未展示它们,以使对 Espresso 使用不熟练的读者更容易理解。

本章的每个部分在此项目上都有一个对应的 Git 分支,因此如果您想看到每个部分结束时的代码是什么样子,只需切换到相应的分支即可:

此外,在本地,当您克隆存储库后,可以使用以下 Git 命令检出相应的分支:

git checkout Character_search

如果您有本书的电子版本,并且想通过复制和粘贴代码的方式制作整个应用程序,那么您可以这样做,但请记住将文件放在对应包的文件夹中。这样,您将保持项目的清晰结构。

请注意,如果您将书中的代码放在其他文件夹中,将会显示警告:

您可以故意将文件放在任何文件夹中,因为第二个修复建议是将文件移动到与定义的包对应的路径中:

您可以使用它将文件移动到正确的位置。

创建一个空项目

在我们开始实现功能之前,我们需要创建一个空的 Kotlin Android 项目,其中只有一个活动,MainActivty。这个过程在第一章中已经描述过了,开始你的 Kotlin 冒险。因此,我们不需要深入描述它,但我们会展示在 Android Studio 3.0 中的步骤是什么:

  1. 为新项目设置名称、包和位置。记得勾选包括 Kotlin 支持选项:.

  1. 我们可以选择其他最小的 Android 版本,但在这个例子中,我们将设置 API 16:

  1. 选择一个模板。我们不需要这些模板中的任何一个,所以我们应该从空活动开始

  1. 命名新创建的活动。我们可以保留第一个视图命名为MainActivity :

对于 Android Studio 3.x 之前的版本,我们需要遵循稍微不同的步骤:

使用空的Activity从模板创建项目。

  1. 配置项目中的 Kotlin(例如,Ctrl/Cmd + Shift + A和配置项目中的 Kotlin)。

  2. 将所有 Java 类转换为 Kotlin(例如,在MainActivityCtrl/Cmd+Shift+A和将 Java 文件转换为 Kotlin 文件)。

经过这些步骤,我们将拥有一个使用空 Activity 创建的 Kotlin Android 应用:

角色画廊

在这一部分,我们将实现一个单一用例——启动应用后,用户可以看到一个角色画廊。

这是一个相当复杂的用例,因为它需要呈现视图、与 API 进行网络连接和实现业务规则。因此,我们将把它分成以下任务:

  • 视图实现

  • 与 API 通信

  • 角色显示的业务逻辑实现

  • 把所有东西放在一起

这样的任务要容易实现得多。让我们依次实现它们。

视图实现

让我们从视图实现开始。在这里,我们将定义角色列表的外观。为了测试目的,我们还将定义一些角色并显示它们。

让我们从MainActivity布局实现开始。我们将使用RecyclerView来显示一个元素列表。RecyclerView布局分布在一个单独的依赖项中,我们需要将其添加到app模块的build.gradle文件中:

implementation "com.android.support:recyclerview-v7:$android_support_version" 

android_support_version实例是一个尚未定义的变量。其背后的原因是所有 Android 支持库的版本应该是相同的,当我们将这个版本号提取为一个分隔变量时,就更容易管理了。这就是为什么我们应该用对android_support_version的引用来替换每个 Android 支持库的硬编码版本:

implementation "com.android.support:appcompat-  

    v7:$android_support_version" 
implementation "com.android.support:design:$android_support_version" 
implementation "com.android.support:support-

    v4:$android_support_version" 
implementation "com.android.support:recyclerview-

    v7:$android_support_version" 

并且我们需要设置支持库版本值。良好的做法是在项目的build*.*gradle文件中的buildscript部分定义它,在kotlin*_*version定义之后:

ext.kotlin_version = '1.1.4-2' 
ext.android_support_version = "26.0.1" 

现在我们可以开始实现MainActivity布局。这是我们想要实现的效果:

我们将把角色元素放入RecyclerView中,打包到SwipeRefreshLayout中以允许滑动刷新。此外,为了满足 Marvel 的版权要求,需要有一个呈现的标签,告知数据是由 Marvel 提供的。布局activity_mainres/layout/activity_main.xml)应该被替换为以下定义:

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout  

   android:id="@+id/charactersView" 
   android:layout_width="match_parent" 
   android:layout_height="match_parent" 
   android:background="@android:color/white" 
   android:fitsSystemWindows="true"> 

   <android.support.v4.widget.SwipeRefreshLayout  

       android:id="@+id/swipeRefreshView" 
       android:layout_width="match_parent" 
       android:layout_height="match_parent"> 

       <android.support.v7.widget.RecyclerView 
           android:id="@+id/recyclerView" 
           android:layout_width="match_parent" 
           android:layout_height="match_parent" 
           android:scrollbars="vertical" /> 

   </android.support.v4.widget.SwipeRefreshLayout> 

   <TextView 
       android:layout_width="match_parent" 
       android:layout_height="wrap_content" 
       android:layout_alignParentBottom="true" 
       android:background="@android:color/white" 
       android:gravity="center" 
       android:text="@string/marvel_copyright_notice" /> 
</RelativeLayout> 

我们需要在字符串(res/values/strings.xml)中添加版权声明:

<string name="marvel_copyright_notice">

    Data provided by Marvel. © 2017 MARVEL

</string> 

这是一个预览:

下一步是定义项目视图。我们希望每个元素都是正方形的。为了做到这一点,我们需要定义一个可以保持正方形形状的视图(将其放在view/views中):

package com.sample.marvelgallery.view.views 

import android.util.AttributeSet 
import android.widget.FrameLayout 
import android.content.Context 

class SquareFrameLayout @JvmOverloads constructor( // 1 
       context: Context, 
       attrs: AttributeSet? = null, 
       defStyleAttr: Int = 0 
) : FrameLayout(context, attrs, defStyleAttr) { 

   override fun onMeasure(widthMeasureSpec: Int, 

   heightMeasureSpec: Int) { 
       super.onMeasure(widthMeasureSpec, widthMeasureSpec) // 2 
   } 
} 
  1. 使用JvmOverloads注解,我们避免了通常用于在 Android 中定义自定义视图的望远镜构造函数。这在第四章中有描述,类和对象

  2. 我们强制元素始终具有与宽度相同的高度。

使用SquareFrameLayout,我们可以定义画廊项目的布局。这就是我们想要的样子:

我们需要定义ImageView来显示角色图像,以及TextView来显示其名称。虽然SquareFrameLayout实际上是具有固定高度的FrameLayout,但它的子元素(图像和文本)默认情况下是一个在另一个上面。让我们将布局添加到res/layout文件夹中的item_character.xml文件中:

// ./res/layout/item_character.xml 

<com.sample.marvelgallery.view.views.SquareFrameLayout  

   android:layout_width="match_parent" 
   android:layout_height="wrap_content" 
   android:gravity="center_horizontal" 
   android:orientation="horizontal" 
   android:padding="@dimen/element_padding"> 

   <ImageView 
       android:id="@+id/imageView" 
       android:layout_width="match_parent" 
       android:layout_height="match_parent"/> 

   <TextView 
       android:id="@+id/textView" 
       android:layout_width="match_parent" 
       android:layout_height="match_parent" 
       android:gravity="center" 
       android:paddingLeft="10dp" 
       android:paddingRight="10dp" 
       android:shadowColor="#111" 
       android:shadowDx="5" 
       android:shadowDy="5" 
       android:shadowRadius="0.01" 
       android:textColor="@android:color/white" 
       android:textSize="@dimen/standard_text_size" 
       tools:text="Some name" /> 
</com.sample.marvelgallery.view.views.SquareFrameLayout> 

请注意,我们还在dimens中定义的element_padding等值。让我们将它们添加到res/values文件夹中的dimen.xml文件中:

<?xml version="1.0" encoding="utf-8"?> 
<resources> 
   <dimen name="character_header_height">240dp</dimen> 
   <dimen name="standard_text_size">20sp</dimen> 
   <dimen name="character_description_padding">10dp</dimen> 
   <dimen name="element_padding">10dp</dimen> 
</resources> 

正如我们所看到的,每个元素都需要显示角色的名称和图像。因此,角色的模型需要包含这两个属性。让我们为角色定义一个简单的模型:

package com.sample.marvelgallery.model 

data class MarvelCharacter( 
       val name: String, 
       val imageUrl: String 
) 

要使用RecyclerView显示元素列表,我们需要实现RecyclerView列表和一个项目适配器。列表适配器用于管理列表中的所有元素,而项目适配器是单个项目类型的适配器。在这里,我们只需要一个项目适配器,因为我们显示单一类型的项目。然而,最好假设在将来可能会有其他类型的元素在这个列表上,例如漫画或广告。列表适配器也是一样--在这个例子中我们只需要一个,但在大多数项目中不止一个列表,最好将通用行为提取到一个单独的抽象类中。

虽然这个例子旨在展示 Kotlin 如何在更大的项目中使用,我们将定义一个抽象列表适配器,我们将其命名为RecyclerListAdapter,以及一个抽象项目适配器,我们将其命名为ItemAdapter。这是ItemAdapter的定义:

package com.sample.marvelgallery.view.common 

import android.support.v7.widget.RecyclerView 
import android.support.annotation.LayoutRes 
import android.view.View 

abstract class ItemAdapter<T : RecyclerView.ViewHolder>

(@LayoutRes open val layoutId: Int) { // 1 

   abstract fun onCreateViewHolder(itemView: View): T // 2 

   @Suppress("UNCHECKED_CAST") // 1 
   fun bindViewHolder(holder: RecyclerView.ViewHolder) { 
       (holder as T).onBindViewHolder() // 1 
   } 

   abstract fun T.onBindViewHolder() // 1, 3 
} 
  1. 我们需要将持有者作为类型参数传递,以允许直接对其字段进行操作。持有者是在onCreateViewHolder中创建的,因此我们知道它的类型将始终是类型参数T。因此,我们可以在bindViewHolder上将持有者转换为T并将其用作onBindViewHolder的接收器对象。@Suppress("UNCHECKED_CAST")的抑制只是为了在我们知道可以在这种情况下安全转换时隐藏警告。

  2. 用于创建视图持有者的函数。在大多数情况下,它将是一个只调用构造函数的单表达式函数。

  3. onBindViewHolder函数中,我们将设置 item 视图上的所有值。

这是RecyclerListAdapter的定义:

package com.sample.marvelgallery.view.common 

import android.support.v7.widget.RecyclerView 
import android.view.LayoutInflater 
import android.view.ViewGroup 

open class RecyclerListAdapter( // 1 
       var items List<AnyItemAdapter> = listOf() 
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { 

   override final fun getItemCount() = items.size // 4 

   override final fun getItemViewType(position: Int) = 

       items[position].layoutId // 3, 4 

   override final fun onCreateViewHolder(parent: ViewGroup, 

       layoutId: Int): RecyclerView.ViewHolder { // 4 

   val itemView = LayoutInflater.from(parent.context)

       .inflate(layoutId, parent, false) 
       return items.first 

       { it.layoutId == layoutId }.onCreateViewHolder(itemView) // 3 
   } 

   override final fun onBindViewHolder

   (holder: RecyclerView.ViewHolder, position: Int) { // 4 
       items[position].bindViewHolder(holder) 
   } 
} 

typealias AnyItemAdapter = ItemAdapter 

    <out RecyclerView.ViewHolder> // 5 
  1. 类是open而不是abstract,因为它可以被初始化和使用而不需要任何子类。我们定义子类是为了允许我们为不同的列表定义自定义方法。

  2. 我们将项目保存在列表中。

  3. 我们将使用布局来区分项目类型。因此,我们不能在同一个列表上使用具有相同布局的两个项目适配器,但这个解决方案简化了很多事情。

  4. 方法是RecyclerView.Adapter的重写方法,但它们还使用final修饰符来限制它们在子类中的重写。所有扩展RecyclerListAdapter的列表适配器都应该操作项目。

  5. 我们定义类型别名来简化任何ItemAdapter的定义。

使用上述定义,我们可以定义MainListAdapter(角色列表的适配器)和CharacterItemAdapter(列表上项目的适配器)。这是MainListAdapter的定义:

package com.sample.marvelgallery.view.main 

import com.sample.marvelgallery.view.common.AnyItemAdapter 
import com.sample.marvelgallery.view.common.RecyclerListAdapter 

class MainListAdapter(items: List<AnyItemAdapter>) : RecyclerListAdapter(items) 

在这个项目中,我们不需要在MainListAdapter中定义任何特殊方法,但是为了展示定义它们有多容易,这里呈现了具有额外添加和删除方法的MainListAdapter

class MainListAdapter(items: List<AnyItemAdapter>) : RecyclerListAdapter(items) { 

   fun add(itemAdapter: AnyItemAdapter) { 
       items += itemAdapter) 
       val index = items.indexOf(itemAdapter) 
       if (index == -1) return 
       notifyItemInserted(index) 
   } 

   fun delete(itemAdapter: AnyItemAdapter) { 
       val index = items.indexOf(itemAdapter) 
       if (index == -1) return 
       items -= itemAdapter 
       notifyItemRemoved(index) 
   } 
 }    

这是CharacterItemAdapter的定义:

package com.sample.marvelgallery.view.main 

import android.support.v7.widget.RecyclerView 
import android.view.View 
import android.widget.ImageView 
import android.widget.TextView 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.view.common.ItemAdapter 
import com.sample.marvelgallery.view.common.bindView 
import com.sample.marvelgallery.view.common.loadImage 

class CharacterItemAdapter( 
       val character: MarvelCharacter // 1 
) : ItemAdapter<CharacterItemAdapter.ViewHolder>(R.layout.item_character) { 

   override fun onCreateViewHolder(itemView: View) = ViewHolder(itemView) 

   override fun ViewHolder.onBindViewHolder() { // 2 
       textView.text = character.name 
       imageView.loadImage(character.imageUrl) // 3 
   } 

   class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)  

   { 
       val textView by bindView<TextView>(R.id.textView) // 4 
       val imageView by bindView<ImageView>(R.id.imageView) // 4 
   } 
} 
  1. MarvelCharacter通过构造函数传递。

  2. onBindViewHolder方法用于设置视图。它被定义为ItemAdapter中的抽象成员扩展函数,由于这样,现在我们可以在其主体内明确使用textViewimageView

  3. loadImage函数尚未定义。我们稍后将其定义为扩展函数。

  4. 在视图持有者中,我们使用bindView函数将属性绑定到视图元素,该函数很快将被定义。

在内部,我们使用尚未定义的函数loadImagebindViewbindView是一个顶级扩展函数,用于RecyclerView.ViewHolder,它提供了一个懒惰的委托,该委托通过其 ID 找到视图:

// ViewExt.kt 
package com.sample.marvelgallery.view.common 

import android.support.v7.widget.RecyclerView 
import android.view.View 

fun <T : View> RecyclerView.ViewHolder.bindView(viewId: Int)  
      = lazy { itemView.findViewById<T>(viewId) } 

我们还需要定义loadImage扩展函数,它将帮助我们从 URL 下载图像并将其放入ImageView中。用于此目的的两个典型库是PicassoGlide。我们将使用 Glide,并且为此,我们需要在build.gradle中添加依赖项:

implementation "com.android.support:recyclerview-

v7:$android_support_version" 
implementation "com.github.bumptech.glide:glide:$glide_version" 

在项目build.gradle中指定版本:

ext.android_support_version = "26.0.0" 
ext.glide_version = "3.8.0" 

AndroidManifest中添加使用互联网的权限:

<manifest  
   package="com.sample.marvelgallery"> 
   <uses-permission android:name="android.permission.INTERNET" /> 
   <application 
... 

最后,我们可以为ImaveView类定义loadImage扩展函数:

// ViewExt.kt 
package com.sample.marvelgallery.view.common 

import android.support.v7.widget.RecyclerView 
import android.view.View 
import android.widget.ImageView 
import com.bumptech.glide.Glide 

fun <T : View> RecyclerView.ViewHolder.bindView(viewId: Int)  
       = lazy { itemView.findViewById<T>(viewId) } 

fun ImageView.loadImage(photoUrl: String) { 
   Glide.with(context) 
           .load(photoUrl) 
           .into(this) 
} 

是时候定义将显示此列表的活动了。我们将使用另一个元素,Kotlin Android 扩展插件。它用于简化从代码访问视图元素。它的使用很简单 - 我们在模块build.gradle中添加kotlin-android-extensions插件:

apply plugin: 'com.android.application' 
apply plugin: 'kotlin-android' 
apply plugin: 'kotlin-android-extensions' 

And we have some view defined in layout: 

<TextView 
   android:id="@+id/nameView" 
   android:layout_width="wrap_content" 
   android:layout_height="wrap_content" /> 

然后我们可以在Activity中导入对此视图的引用:

import kotlinx.android.synthetic.main.activity_main.* 

我们可以直接使用其名称访问View元素,而无需使用findViewById方法或定义注释:

nameView.text = "Some name" 

我们将在项目中的所有活动中使用 Kotlin Android 扩展。现在让我们定义MainActivity以显示带有图像的角色列表:

package com.sample.marvelgallery.view.main 

import android.os.Bundle 
import android.support.v7.app.AppCompatActivity 
import android.support.v7.widget.GridLayoutManager 
import android.view.Window 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.model.MarvelCharacter 
import kotlinx.android.synthetic.main.activity_main.* 

class MainActivity : AppCompatActivity() { 

   private val characters = listOf( // 1 
       MarvelCharacter(name = "3-D Man", imageUrl = "http://i.annihil.us/u/prod/marvel/i/mg/c/e0/535fecbbb9784.jpg"), 
       MarvelCharacter(name = "Abomination (Emil Blonsky)", imageUrl = "http://i.annihil.us/u/prod/marvel/i/mg/9/50/4ce18691cbf04.jpg") 
   ) 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       requestWindowFeature(Window.FEATURE_NO_TITLE) // 2 
       setContentView(R.layout.activity_main) 
       recyclerView.layoutManager = GridLayoutManager(this, 2) // 3 
       val categoryItemAdapters = characters

       .map(::CharacterItemAdapter) // 4 
       recyclerView.adapter = MainListAdapter(categoryItemAdapters) 
   } 
} 
  1. 在这里,我们定义了一个临时的角色列表以显示。

  2. 我们使用此窗口功能,因为我们不想显示标题。

  3. 我们使用GridLayoutManager作为RecyclerView布局管理器以实现网格效果。

  4. 我们正在使用CharacterItemAdapter构造函数引用从字符创建项目适配器。

现在我们可以编译项目,然后我们会看到以下屏幕:

网络定义

到目前为止,所呈现的数据是在应用程序内部硬编码的,但我们希望改为使用 Marvel API 的数据。为此,我们需要定义一些网络机制,以从服务器检索数据。我们将使用Retrofit,这是一个流行的 Android 库,用于简化网络操作,以及 RxJava,这是一个用于响应式编程的流行库。对于这两个库,我们将仅使用基本功能,以使其使用尽可能简单。要使用它们,我们需要在模块build.gradle中添加以下依赖项:

dependencies { 
   implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:

   $kotlin_version" 
   implementation "com.android.support:appcompat-v7:

   $android_support_version" 
   implementation "com.android.support:recyclerview-v7:

   $android_support_version" 
   implementation "com.github.bumptech.glide:glide:$glide_version" 

   // RxJava 
   implementation "io.reactivex.rxjava2:rxjava:$rxjava_version" 

   // RxAndroid 
   implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version" 

   // Retrofit 
   implementation(["com.squareup.retrofit2:retrofit:$retrofit_version", 
                   "com.squareup.retrofit2:adapter- 

                    rxjava2:$retrofit_version", 
                   "com.squareup.retrofit2:converter-

                    gson:$retrofit_version", 
                   "com.squareup.okhttp3:okhttp:$okhttp_version", 
                   "com.squareup.okhttp3:logging-

                   interceptor:$okhttp_version"]) 

  testImplementation 'junit:junit:4.12' 
  androidTestImplementation 

  'com.android.support.test:runner:1.0.0' 
  androidTestImplementation   

  'com.android.support.test.espresso:espresso-core:3.0.0' 
} 

在项目build.gradle中定义版本定义:

ext.kotlin_version = '1.1.3-2' 
ext.android_support_version = "26.0.0" 
ext.glide_version = "3.8.0" 
ext.retrofit_version = '2.2.0' 
ext.okhttp_version = '3.6.0' 
ext.rxjava_version = "2.1.2" 
ext.rxandroid_version = '2.0.1' 

我们已经在AndroidManifest中定义了互联网权限,因此不需要添加它。简单的Retrofit定义可能如下所示:

val retrofit by lazy { makeRetrofit() } // 1 

private fun makeRetrofit(): Retrofit = Retrofit.Builder() 
       .baseUrl("http://gateway.marvel.com/v1/public/") // 2 
       .build() 
  1. 我们可以将retrofit实例保留为惰性顶级属性。

  2. 在这里我们定义baseUrl

但是 Retrofit 还有一些额外的要求需要满足。我们需要添加转换器以将 Retrofit 与 RxJava 一起使用,并将对象序列化为 JSON 进行发送。我们还需要拦截器,这些拦截器将用于提供 Marvel API 所需的标头和额外查询。这是一个小应用程序,因此我们可以将所有所需的元素定义为顶级函数。完整的 Retrofit 定义将如下所示:

// Retrofit.kt 
package com.sample.marvelgallery.data.network.provider 

import com.google.gson.Gson 
import okhttp3.OkHttpClient 
import retrofit2.Retrofit 
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 
import retrofit2.converter.gson.GsonConverterFactory 
import java.util.concurrent.TimeUnit 

val retrofit by lazy { makeRetrofit() } 

private fun makeRetrofit(): Retrofit = Retrofit.Builder() 
       .baseUrl("http://gateway.marvel.com/v1/public/") 
       .client(makeHttpClient()) 
       .addConverterFactory(GsonConverterFactory.create(Gson())) // 1 
       .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 2 
       .build() 

private fun makeHttpClient() = OkHttpClient.Builder() 
       .connectTimeout(60, TimeUnit.SECONDS) // 3 
       .readTimeout(60, TimeUnit.SECONDS) // 4 
       .addInterceptor(makeHeadersInterceptor()) // 5 
       .addInterceptor(makeAddSecurityQueryInterceptor()) // 6 
       .addInterceptor(makeLoggingInterceptor()) // 7 
       .build() 
  1. 添加一个允许使用 GSON 库对对象 JSON 进行序列化和反序列化的转换器。

  2. 添加一个转换器,它将允许 RxJava2 类型(Observable,Single)作为网络请求返回值的可观察对象。

  3. 我们添加自定义拦截器。我们需要定义它们所有。

让我们定义所需的拦截器。makeHeadersInterceptor用于为每个请求添加标准标头:

// HeadersInterceptor.kt 
package com.sample.marvelgallery.data.network.provider 

import okhttp3.Interceptor 

fun makeHeadersInterceptor() = Interceptor { chain -> // 1 
   chain.proceed(chain.request().newBuilder() 
           .addHeader("Accept", "application/json") 
           .addHeader("Accept-Language", "en") 
           .addHeader("Content-Type", "application/json") 
           .build()) 
}
  1. 拦截器是 SAM,因此我们可以使用 SAM 构造函数来定义它。

makeLoggingInterceptor函数用于在调试模式下运行应用程序时在控制台上显示日志:

// LoggingInterceptor.kt 
package com.sample.marvelgallery.data.network.provider 

import com.sample.marvelgallery.BuildConfig 
import okhttp3.logging.HttpLoggingInterceptor 

fun makeLoggingInterceptor() = HttpLoggingInterceptor().apply { 
   level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY 

           else HttpLoggingInterceptor.Level.NONE 
} 

makeAddRequiredQueryInterceptor函数更复杂,因为它用于提供 Marvel API 用于验证用户的查询参数。这些参数需要使用 MD5 算法计算的哈希。它还需要来自 Marvel API 的公钥和私钥。每个人都可以在developer.marvel.com/生成自己的密钥。生成密钥后,我们需要将它们放在gradle.properties文件中:

org.gradle.jvmargs=-Xmx1536m 
marvelPublicKey=REPLEACE_WITH_YOUR_PUBLIC_MARVEL_KEY 
marvelPrivateKey=REPLEACE_WITH_YOUR_PRIVATE_MARVEL_KEY 

还在 Android 的defaultConfig部分的模块build.gradle中添加以下定义:

defaultConfig { 
   applicationId "com.sample.marvelgallery" 
   minSdkVersion 16 
   targetSdkVersion 26 
   versionCode 1 
   versionName "1.0" 
   testInstrumentationRunner 

   "android.support.test.runner.AndroidJUnitRunner" 
   buildConfigField("String", "PUBLIC_KEY", "\"${marvelPublicKey}\"") 
   buildConfigField("String", "PRIVATE_KEY", "\"${marvelPrivateKey}\"") 
} 

项目重建后,您将能够通过BuildConfig.PUBLIC_KEYBuildConfig.PRIVATE_KEY访问这些值。使用这些密钥,我们可以生成 Marvel API 所需的查询参数:

// QueryInterceptor.kt 
package com.sample.marvelgallery.data.network.provider 

import com.sample.marvelgallery.BuildConfig 
import okhttp3.Interceptor 

fun makeAddSecurityQueryInterceptor() = Interceptor { chain -> 
   val originalRequest = chain.request() 
   val timeStamp = System.currentTimeMillis() 

   // Url customization: add query parameters 
   val url = originalRequest.url().newBuilder() 
           .addQueryParameter("apikey", BuildConfig.PUBLIC_KEY) // 1 
           .addQueryParameter("ts", "$timeStamp") // 1 
           .addQueryParameter("hash", calculatedMd5(timeStamp.toString() + BuildConfig.PRIVATE_KEY + BuildConfig.PUBLIC_KEY)) // 1 
           .build() 

   // Request customization: set custom url 
   val request = originalRequest 
           .newBuilder() 
           .url(url) 
           .build() 

   chain.proceed(request) 
} 
  1. 我们需要提供三个额外的查询:
  • apikey:只包括我们的公钥。

  • ts:只包含设备时间的毫秒数。它用于提高下一个查询中提供的哈希的安全性。

  • hash:这是从时间戳、私钥和公钥依次计算 MD5 哈希的String

这是用于计算 MD5 哈希的函数的定义:

// MD5.kt 
package com.sample.marvelgallery.data.network.provider 

import java.math.BigInteger 
import java.security.MessageDigest 

/** 
* Calculate MD5 hash for text 
* @param timeStamp Current timeStamp 
* @return MD5 hash string 
*/ 
fun calculatedMd5(text: String): String { 
   val messageDigest = getMd5Digest(text) 
   val md5 = BigInteger(1, messageDigest).toString(16) 
   return "0" * (32 - md5.length) + md5 // 1 
} 

private fun getMd5Digest(str: String): ByteArray = MessageDigest.getInstance("MD5").digest(str.toByteArray()) 

private operator fun String.times(i: Int) = (1..i).fold("") { acc, _ -> acc + this } 
  1. 我们正在使用 times 扩展运算符来填充哈希,如果它比 32 短。

我们已经定义了拦截器,因此我们可以定义实际的 API 方法。Marvel API 包含许多表示字符、列表等的数据模型。我们需要将它们定义为单独的类。这样的类称为数据传输对象DTOs)。我们将定义我们需要的对象:

package com.sample.marvelgallery.data.network.dto 

class DataContainer<T> { 
   var results: T? = null 
} 

package com.sample.marvelgallery.data.network.dto 

class DataWrapper<T> { 
   var data: DataContainer<T>? = null 
} 

package com.sample.marvelgallery.data.network.dto 

class ImageDto { 

   lateinit var path: String // 1 
   lateinit var extension: String // 1 

   val completeImagePath: String 
       get() = "$path.$extension" 
} 

package com.sample.marvelgallery.data.network.dto 

class CharacterMarvelDto { 
   lateinit var name: String // 1 
   lateinit var thumbnail: ImageDto // 1 

   val imageUrl: String 
       get() = thumbnail.completeImagePath 
} 
  1. 对于可能未提供的值,我们应该设置默认值。必须提供的值可能会用lateinit前缀。

Retrofit 使用反射来创建基于接口定义的 HTTP 请求。这是我们如何实现定义 HTTP 请求的接口:

package com.sample.marvelgallery.data.network 

import com.sample.marvelgallery.data.network.dto.CharacterMarvelDto 
import com.sample.marvelgallery.data.network.dto.DataWrapper 
import io.reactivex.Single 
import retrofit2.http.GET 
import retrofit2.http.Query 

interface MarvelApi { 

   @GET("characters") 
   fun getCharacters( 
           @Query("offset") offset: Int?, 
           @Query("limit") limit: Int? 
   ): Single<DataWrapper<List<CharacterMarvelDto>>> 
}  

有了这样的定义,我们最终可以得到一个字符列表:

retrofit.create(MarvelApi::class.java) // 1 

    .getCharacters(0, 100) // 2

    .subscribe({ /* code */ }) // 3 
  1. 我们使用retrofit实例来创建一个对象,根据MarvelApi接口定义进行 HTTP 请求。

  2. 我们创建一个准备发送到 API 的可观察对象。

  3. 通过subscribe,我们发送一个 HTTP 请求并开始监听响应。第一个参数是在成功接收响应时调用的回调函数。

这样的网络定义可能已经足够了,但我们可能会实现得更好。最大的问题是我们现在需要操作 DTO 对象,而不是我们自己的数据模型对象。对于映射,我们应该定义一个额外的层。存储库模式用于此目的。当我们实现单元测试时,这种模式也非常有帮助,因为我们可以模拟存储库而不是整个 API 定义。这是我们想要的存储库定义:

package com.sample.marvelgallery.data 

import com.sample.marvelgallery.model.MarvelCharacter 
import io.reactivex.Single 

interface MarvelRepository { 

   fun getAllCharacters(): Single<List<MarvelCharacter>> 
} 

And here is the implementation of MarvelRepository: 

package com.sample.marvelgallery.data 

import com.sample.marvelgallery.data.network.MarvelApi 
import com.sample.marvelgallery.data.network.provider.retrofit 
import com.sample.marvelgallery.model.MarvelCharacter 
import io.reactivex.Single 

class MarvelRepositoryImpl : MarvelRepository { 

   val api = retrofit.create(MarvelApi::class.java) 

   override fun getAllCharacters(): Single<List<MarvelCharacter>> = api.getCharacters( 
           offset = 0, 
           limit = elementsOnListLimit 
   ).map { 
       it.data?.results.orEmpty().map(::MarvelCharacter) // 1 
   } 

   companion object { 
       const val elementsOnListLimit = 50 
   } 
} 
  1. 我们正在获取 DTO 元素的列表,并使用构造函数引用将其映射到MarvelCharacter

为使其工作,我们需要在MarvelCharacter中定义一个额外的构造函数,以CharacterMarvelDto作为参数:

package com.sample.marvelgallery.model 

import com.sample.marvelgallery.data.network.dto.CharacterMarvelDto 

class MarvelCharacter( 
       val name: String, 
       val imageUrl: String 
) { 

   constructor(dto: CharacterMarvelDto) : this( 
           name = dto.name, 
           imageUrl = dto.imageUrl 
   ) 
} 

提供MarvelRepository实例的不同方法。在最常见的实现中,具体的MarvelRepository实例作为构造函数参数传递给Presenter。但是对于 UI 测试(如 Espresso 测试)呢?我们不想测试 Marvel API,也不想使 UI 测试依赖于它。解决方案是制作一个机制,在正常运行时生成标准实现,但也允许我们为测试目的设置不同的实现。我们将制作以下通用机制的实现(将其放在数据中):

package com.sample.marvelgallery.data 

abstract class Provider<T> { 

   abstract fun creator(): T 

   private val instance: T by lazy { creator() } 
   var testingInstance: T? = null 

   fun get(): T = testingInstance ?: instance 
} 

我们可以使用一些依赖注入库,如DaggerKodein,而不是定义自己的Provider。在 Android 开发中,Dagger 用于此类目的非常普遍,但我们决定不在此示例中包含它,以避免给不熟悉该库的开发人员增加额外的复杂性。

我们可以使MarvelRepository的伴生对象提供者扩展上述类:

package com.sample.marvelgallery.data 

import com.sample.marvelgallery.model.MarvelCharacter 
import io.reactivex.Single 

interface MarvelRepository { 

   fun getAllCharacters(): Single<List<MarvelCharacter>> 

   companion object : Provider<MarvelRepository>() { 
       override fun creator() = MarvelRepositoryImpl() 
   } 
} 

由于前面的定义,我们可以使用MarvelRepository的伴生对象来获取MarvelRepository的实例:

val marvelRepository = MarvelRepository.get()  

它将是 MarvelRepositoryImpl 的延迟实例,直到有人设置testingInstance属性的非空值为止:

MarvelRepository.get() // Returns instance of MarvelRepositoryImpl 

MarvelRepository.testingInstance= object: MarvelRepository { 
   override fun getAllCharacters(): Single<List<MarvelCharacter>>  
         = Single.just(emptyList()) 
} 

MarvelRepository.get() // returns an instance of an anonymous class in which the returned list is always empty. 

这样的构造对使用 Espresso 进行 UI 测试非常有用。它在项目中用于元素覆盖,并且可以在 GitHub 上找到。为了让不熟悉测试的开发人员更容易理解,本节中没有介绍它。如果你想看到它,可以在github.com/MarcinMoskala/MarvelGallery/blob/master/app/src/androidTest/java/com/sample/marvelgallery/MainActivityTest.kt找到。

最后让我们通过实现角色画廊显示的业务逻辑来将这个存储库与视图连接起来。

业务逻辑实现

我们已经实现了视图和存储库部分,现在是时候最终实现业务逻辑了。在这一点上,我们只需要获取角色列表并在用户进入屏幕或刷新时显示它。我们将使用一种称为Model-View-PresenterMVP)的架构模式从视图实现中提取这些业务逻辑规则。以下是简化的规则:

  • Model:这是负责管理数据的层。模型的责任包括使用 API、缓存数据、管理数据库等。

  • Presenter:Presenter 是模型和视图之间的中间人,它应该包含所有的演示逻辑。Presenter 负责对用户交互做出反应,使用和更新模型和视图。

  • View:这负责呈现数据并将用户交互事件转发给 Presenter。

在我们实现这种模式时,我们将 Activity 视为视图,并且对于每个视图,我们需要创建一个 Presenter。编写单元测试来检查业务逻辑规则是否正确实现是一个好的实践。为了简化,我们需要将 Activity 隐藏在一个易于模拟的接口后面,该接口代表了 Presenter 与视图(Activity)的所有可能的交互。此外,我们将在 Activity 中创建所有依赖项(例如MarvelRepository),并通过构造函数将它们作为隐藏在接口后面的对象(例如,将MarvelRepositoryImpl作为MarvelRepository)传递给 Presenter。

在 Presenter 中,我们需要实现以下行为:

  • 当 Presenter 等待响应时,显示加载动画

  • 视图创建后,加载并显示角色列表

  • 调用刷新方法后,加载角色列表

  • 当 API 返回角色列表时,它会显示在视图上

  • 当 API 返回错误时,它会显示在视图上

正如我们所看到的,Presenter 需要通过构造函数获取 View 和MarvelRepository,并且应该指定在视图创建或用户请求列表刷新时将调用的方法:

package com.sample.marvelgallery.presenter 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.view.main.MainView 

class MainPresenter(val view: MainView, val repository: MarvelRepository) { 

   fun onViewCreated() { 
   } 

   fun onRefresh() { 
   } 
} 

视图需要指定用于显示角色列表、显示错误和在视图刷新时显示进度条的方法(在view/main中定义,并将MainActivity移动到view/main):

package com.sample.marvelgallery.view.main.main 

import com.sample.marvelgallery.model.MarvelCharacter 

interface MainView { 
   var refresh: Boolean 
   fun show(items: List<MarvelCharacter>) 
   fun showError(error: Throwable) 
} 

在向 Presenter 添加逻辑之前,让我们先定义两个单元测试:

// test source set 
package com.sample.marvelgallery 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import com.sample.marvelgallery.view.main.MainView 
import io.reactivex.Single 
import org.junit.Assert.assertEquals 
import org.junit.Assert.fail 
import org.junit.Test 

@Suppress("IllegalIdentifier") // 1 
class MainPresenterTest { 

   @Test 
   fun `After view was created, list of characters is loaded and displayed`() { 
       assertOnAction { onViewCreated() }.thereIsSameListDisplayed() 
   } 

   @Test 
   fun `New list is shown after view was refreshed`() { 
       assertOnAction { onRefresh() }.thereIsSameListDisplayed() 
   } 

   private fun assertOnAction(action: MainPresenter.() -> Unit) 
           = PresenterActionAssertion(action) 

   private class PresenterActionAssertion

   (val actionOnPresenter: MainPresenter.() -> Unit) { 

       fun thereIsSameListDisplayed() { 
           // Given 
           val exampleCharacterList = listOf(// 2 
                   MarvelCharacter("ExampleName", "ExampleImageUrl"), 
                   MarvelCharacter("Name1", "ImageUrl1"), 
                   MarvelCharacter("Name2", "ImageUrl2") 
           ) 

           var displayedList: List<MarvelCharacter>? = null 

           val view = object : MainView { //3 
               override var refresh: Boolean = false 

               override fun show(items: List<MarvelCharacter>) { 
                   displayedList = items // 4 
               } 

               override fun showError(error: Throwable) { 
                   fail() //5 
               } 
           } 
           val marvelRepository = object : MarvelRepository { // 3 
               override fun getAllCharacters(): 

                Single<List<MarvelCharacter>> 
                  = Single.just(exampleCharacterList) // 6 
           } 

           val mainPresenter = MainPresenter(view, marvelRepository) 

           // 3 

           // When 
           mainPresenter.actionOnPresenter() // 7 

           // Then 
           assertEquals(exampleCharacterList, displayedList) // 8 
       } 
   } 
} 
  1. Kotlin 单元测试允许使用描述性名称,但会显示警告。需要抑制此警告。

  2. 定义一个要显示的示例角色列表。

  3. 定义一个视图和存储库,并使用它们创建一个 Presenter。

  4. 当显示元素列表时,我们应该将其设置为显示的列表。

  5. 当调用showError时,测试失败。

  6. getAllCharacters 方法只是返回一个示例列表。

  7. 我们在 Presenter 上调用一个定义好的动作。

  8. 我们检查存储库返回的列表是否与显示的列表相同。

为了简化前面的定义,我们可以提取BaseMarvelRepositoryBaseMainView,并将示例数据保存在一个单独的类中:

// test source set 
package com.sample.marvelgallery.helpers 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import io.reactivex.Single 

class BaseMarvelRepository( 
       val onGetCharacters: () -> Single<List<MarvelCharacter>> 
) : MarvelRepository { 

   override fun getAllCharacters() = onGetCharacters() 
} 

// test source set 
package com.sample.marvelgallery.helpers 

import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.view.main.MainView 

class BaseMainView( 
       var onShow: (items: List<MarvelCharacter>) -> Unit = {}, 
       val onShowError: (error: Throwable) -> Unit = {}, 
       override var refresh: Boolean = false 
) : MainView { 

   override fun show(items: List<MarvelCharacter>) { 
       onShow(items) 
   } 

   override fun showError(error: Throwable) { 
       onShowError(error) 
   } 
} 

// test source set 
package com.sample.marvelgallery.helpers 

import com.sample.marvelgallery.model.MarvelCharacter 

object Example { 
   val exampleCharacter = MarvelCharacter

   ("ExampleName", "ExampleImageUrl") 
   val exampleCharacterList = listOf( 
           exampleCharacter, 
           MarvelCharacter("Name1", "ImageUrl1"), 
           MarvelCharacter("Name2", "ImageUrl2") 
   ) 
} 

现在我们可以简化PresenterActionAssertion的定义:

package com.sample.marvelgallery 

import com.sample.marvelgallery.helpers.BaseMainView 
import com.sample.marvelgallery.helpers.BaseMarvelRepository 
import com.sample.marvelgallery.helpers.Example 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import io.reactivex.Single 
import org.junit.Assert.assertEquals 
import org.junit.Assert.fail 
import org.junit.Test 

@Suppress("IllegalIdentifier") 

class MainPresenterTest { 

   @Test 
   fun `After view was created, list of characters is loaded and displayed`() { 
       assertOnAction { onViewCreated() }.thereIsSameListDisplayed() 
   } 

   @Test 
   fun `New list is shown after view was refreshed`() { 
       assertOnAction { onRefresh() }.thereIsSameListDisplayed() 
   } 

   private fun assertOnAction(action: MainPresenter.() -> Unit) 
           = PresenterActionAssertion(action) 

   private class PresenterActionAssertion

   (val actionOnPresenter: MainPresenter.() -> Unit) { 

       fun thereIsSameListDisplayed() { 
           // Given 
           var displayedList: List<MarvelCharacter>? = null 

           val view = BaseMainView( 
                   onShow = { items -> displayedList = items }, 
                   onShowError = { fail() } 
           ) 
           val marvelRepository = BaseMarvelRepository( 
                 onGetCharacters = 

           { Single.just(Example.exampleCharacterList) } 
           ) 

           val mainPresenter = MainPresenter(view, marvelRepository) 

           // When 
           mainPresenter.actionOnPresenter() 

           // Then 
           assertEquals(Example.exampleCharacterList, displayedList) 
       } 
   } 
} 

我们开始测试:

我们会发现它们没有通过:

原因是MainPresenter中的功能尚未实现。满足这个单元测试的最简单的代码如下:

package com.sample.marvelgallery.presenter 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.view.main.MainView 

class MainPresenter(val view: MainView, val repository: MarvelRepository) { 

   fun onViewCreated() { 
       loadCharacters() 
   } 

   fun onRefresh() { 
       loadCharacters() 
   } 

   private fun loadCharacters() { 
       repository.getAllCharacters() 
               .subscribe({ items -> 
                   view.show(items) 
               }) 
   } 
} 

现在我们的测试通过了:

但是以下实现存在两个问题:

  • 在 Android 中不起作用,因为getAllCharacters正在使用网络操作,而不能像这个例子中一样在主线程上运行

  • 如果用户在加载完成之前离开应用程序,我们将会有内存泄漏

为了解决第一个问题,我们需要指定哪些操作应该在哪些线程上运行。网络请求应该在 I/O 线程上运行,我们应该在 Android 主线程上观察(因为我们在回调中改变了视图):

repository.getAllCharacters() 
       .subscribeOn(Schedulers.io()) // 1 
       .observeOn(AndroidSchedulers.mainThread()) // 2 
       .subscribe({ items -> view.show(items) }) 
  1. 我们指定网络请求应该在 IO 线程中运行。

  2. 我们指定回调应该在主线程上启动。

虽然这些是常见的调度程序,但我们可以将它们提取到顶层扩展函数中:

// RxExt.kt 
package com.sample.marvelgallery.data 

import io.reactivex.Single 
import io.reactivex.android.schedulers.AndroidSchedulers 
import io.reactivex.schedulers.Schedulers 

fun <T> Single<T>.applySchedulers(): Single<T> = this 
       .subscribeOn(Schedulers.io()) 
       .observeOn(AndroidSchedulers.mainThread()) 

And use it in MainPresenter: 

repository.getAllCharacters() 
       .applySchedulers() 
       .subscribe({ items -> view.show(items) }) 

测试不允许访问 Android 主线程。因此,我们的测试将无法通过。此外,在单元测试中运行在新线程上的操作并不是我们想要的,因为我们会有问题断言同步。为了解决这些问题,我们需要在单元测试之前覆盖调度程序,使一切都在同一个线程上运行(将其添加到MainPresenterTest类中):

package com.sample.marvelgallery 

import com.sample.marvelgallery.helpers.BaseMainView 
import com.sample.marvelgallery.helpers.BaseMarvelRepository 
import com.sample.marvelgallery.helpers.Example 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import io.reactivex.Single 
import io.reactivex.android.plugins.RxAndroidPlugins 
import io.reactivex.plugins.RxJavaPlugins 
import io.reactivex.schedulers.Schedulers 
import org.junit.Assert.assertEquals 
import org.junit.Assert.fail 
import org.junit.Before 
import org.junit.Test 

@Suppress("IllegalIdentifier") 

class MainPresenterTest { 

   @Before 
   fun setUp() { 
       RxAndroidPlugins.setInitMainThreadSchedulerHandler { 

           Schedulers.trampoline() } 
       RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } 
       RxJavaPlugins.setComputationSchedulerHandler { 

           Schedulers.trampoline() } 
       RxJavaPlugins.setNewThreadSchedulerHandler { 

           Schedulers.trampoline() } 
   } 

   @Test 
   fun `After view was created, list of characters is loaded and 

        displayed`() { 
       assertOnAction { onViewCreated() }.thereIsSameListDisplayed() 
   } 

   @Test 
   fun `New list is shown after view was refreshed`() { 
       assertOnAction { onRefresh() }.thereIsSameListDisplayed() 
   } 

现在单元测试再次通过了:

另一个问题是,如果用户在我们收到服务器响应之前离开应用程序,会出现内存泄漏。一个常见的解决方案是将所有订阅保留在 composite 中,并在用户离开应用程序时将它们全部处理掉:

private var subscriptions = CompositeDisposable() 

fun onViewDestroyed() { 
   subscriptions.dispose() 
} 

在更大的应用程序中,大多数 Presenter 都有一些订阅。因此,收集订阅并在用户销毁视图时处理它们的功能可以被视为常见行为,并在BasePresenter中提取。此外,为了简化流程,我们可以创建一个BaseActivityWithPresenter类,它将在Presenter接口后面保存 Presenter,并在视图被销毁时调用onViewDestroyed方法。让我们在我们的应用程序中定义这个机制。以下是Presenter的定义:

package com.sample.marvelgallery.presenter 

interface Presenter { 
   fun onViewDestroyed() 
} 

以下是BasePresenter的定义:

package com.sample.marvelgallery.presenter 

import io.reactivex.disposables.CompositeDisposable 

abstract class BasePresenter : Presenter { 

   protected var subscriptions = CompositeDisposable() 

   override fun onViewDestroyed() { 
       subscriptions.dispose() 
   } 
} 

以下是BaseActivityWithPresenter的定义:

package com.sample.marvelgallery.view.common 

import android.support.v7.app.AppCompatActivity 
import com.sample.marvelgallery.presenter.Presenter 

abstract class BaseActivityWithPresenter : AppCompatActivity() { 

   abstract val presenter: Presenter 

   override fun onDestroy() { 
       super.onDestroy() 
       presenter.onViewDestroyed() 
   } 
} 

为了简化将新订阅添加到订阅中的定义,我们可以定义一个加法分配运算符:

// RxExt.ext 
package com.sample.marvelgallery.data 

import io.reactivex.Single 
import io.reactivex.android.schedulers.AndroidSchedulers 
import io.reactivex.disposables.CompositeDisposable 
import io.reactivex.disposables.Disposable 
import io.reactivex.schedulers.Schedulers 

fun <T> Single<T>.applySchedulers(): Single<T> = this 
       .subscribeOn(Schedulers.io()) 
       .observeOn(AndroidSchedulers.mainThread()) 

operator fun CompositeDisposable.plusAssign(disposable: Disposable) { 
   add(disposable) 
} 

我们可以使用这两种解决方案来使MainPresenter更安全:

package com.sample.marvelgallery.presenter 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.data.applySchedulers 
import com.sample.marvelgallery.data.plusAssign 
import com.sample.marvelgallery.view.main.MainView 

class MainPresenter( 
       val view: MainView, 
       val repository: MarvelRepository 
) : BasePresenter() { 

   fun onViewCreated() { 
       loadCharacters() 
   } 

   fun onRefresh() { 
       loadCharacters() 
   } 

   private fun loadCharacters() { 
       subscriptions += repository.getAllCharacters() 
               .applySchedulers() 
               .subscribe({ items -> 
                   view.show(items) 
               }) 
   } 
} 

前两个MainPresenter行为已经实现。现在是时候转向下一个--当 API 返回错误时,它会显示在视图上。我们可以将这个要求作为MainPresenterTest中的一个测试添加:

@Test 
fun `New list is shown after view was refreshed`() { 
   assertOnAction { onRefresh() }.thereIsSameListDisplayed() 
} 

@Test 
fun `When API returns error, it is displayed on view`() { 
   // Given 
   val someError = Error() 
   var errorDisplayed: Throwable? = null 
   val view = BaseMainView( 
           onShow = { _ -> fail() }, 
           onShowError = { errorDisplayed = it } 
   ) 
   val marvelRepository = BaseMarvelRepository 

   { Single.error(someError) } 
   val mainPresenter = MainPresenter(view, marvelRepository) 
   // When 
   mainPresenter.onViewCreated() 
   // Then 
   assertEquals(someError, errorDisplayed) 
} 

private fun assertOnAction(action: MainPresenter.() -> Unit) 
       = PresenterActionAssertion(action) 

使这个测试通过的一个简单的改变是在MainPresenter的订阅方法中指定错误处理程序:

subscriptions += repository.getAllCharacters() 
       .applySchedulers() 
       .subscribe({ items -> // onNext 
           view.show(items) 
       }, { // onError 
           view.showError(it) 
       }) 

虽然subscribe是 Java 方法,我们不能使用命名参数约定。这种调用并不真正描述性。这就是为什么我们将在RxExt.kt中定义一个名为subscribeBy的自定义订阅方法:

// Ext.kt

fun <T> Single<T>.applySchedulers(): Single<T> = this

       .subscribeOn(Schedulers.io())

       .observeOn(AndroidSchedulers.mainThread())

fun <T> Single<T>.subscribeBy(

       onError: ((Throwable) -> Unit)? = null,

       onSuccess: (T) -> Unit

): Disposable = subscribe(onSuccess, { onError?.invoke(it) })

我们将使用它而不是订阅:

subscriptions += repository.getAllCharacters()

       .applySchedulers()

       .subscribeBy(

               onSuccess = view::show,

               onError = view::showError

      )

subscribeBy的完整版本定义了不同的 RxJava 类型(如 Observable、Flowable 等),以及许多其他有用的 Kotlin 扩展到 RxJava,可以在RxKotlin库中找到(github.com/ReactiveX/RxKotlin)。

为了显示和隐藏列表加载,我们将定义额外的监听器来监听在处理之前和之后总是发生的事件:

subscriptions += repository.getAllCharacters()

       .applySchedulers()

       .doOnSubscribe { view.refresh = true },}

               onSuccess = view::show,

       .doFinally { view.refresh = false }

       .subscribeBy(

                     onSuccess = view::show,

                     onError = view::showError,

                onFinish = { view.refresh = false }

       )

测试又通过了:

subscribe方法变得越来越难以阅读,但我们将解决这个问题,还有另一个业务规则,其定义如下--当 Presenter 等待响应时,会显示刷新。在MainPresenterTest中定义其单元测试:

package com.sample.marvelgallery 

import com.sample.marvelgallery.helpers.BaseMainView 
import com.sample.marvelgallery.helpers.BaseMarvelRepository 
import com.sample.marvelgallery.helpers.Example 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import io.reactivex.Single 
import io.reactivex.android.plugins.RxAndroidPlugins 
import io.reactivex.plugins.RxJavaPlugins 
import io.reactivex.schedulers.Schedulers 
import org.junit.Assert.* 
import org.junit.Before 
import org.junit.Test 

@Suppress("IllegalIdentifier") 

class MainPresenterTest { 

   @Test 
   fun `When presenter is waiting for response, refresh is displayed`()  

   { 
       // Given 
       val view = BaseMainView(refresh = false) 
       val marvelRepository = BaseMarvelRepository( 
               onGetCharacters = { 
                   Single.fromCallable { 
                       // Then 
                       assertTrue(view.refresh) // 1 
                       Example.exampleCharacterList 
                   } 
               } 
       ) 
       val mainPresenter = MainPresenter(view, marvelRepository) 
       view.onShow = { _ -> 
           // Then 
           assertTrue(view.refresh) // 1 
       } 
       // When 
       mainPresenter.onViewCreated() 
       // Then 
       assertFalse(view.refresh) // 1 
   } 
 } 
  1. 我们期望在网络请求期间和显示元素时刷新显示,但在处理完成后不刷新。

我们期望在网络请求期间和显示元素时刷新显示,但在处理完成后不刷新。

在 RxJava2 的这个版本中,回调内的断言不会破坏测试,而是在执行报告中显示错误:

可能在未来的版本中,将可以添加一个处理程序,允许从回调内部使测试失败。

为了显示和隐藏列表加载,我们将定义额外的监听器来监听在处理之前和之后总是发生的事件:

subscriptions += repository.getAllCharacters()

       .applySchedulers()

       .doOnSubscribe { view.refresh = true }

       .doFinally { view.refresh = false }

       .subscribeBy(

                     onSuccess = view::show,

                     onError = view::showError

        )

在这些更改之后,所有测试又通过了:

现在我们有一个完全功能的 Presenter、网络和视图。是时候把它们全部连接起来,完成第一个用例的实现了。

把它们放在一起

我们已经准备好在项目中使用MainPresenter。现在我们需要在MainActivity中使用它:

package com.sample.marvelgallery.view.main 

import android.os.Bundle 
import android.support.v7.widget.GridLayoutManager 
import android.view.Window 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import com.sample.marvelgallery.view.common.BaseActivityWithPresenter 
import com.sample.marvelgallery.view.common.bindToSwipeRefresh 
import com.sample.marvelgallery.view.common.toast 
import kotlinx.android.synthetic.main.activity_main.* 

class MainActivity : BaseActivityWithPresenter(), MainView { // 1 

   override var refresh by bindToSwipeRefresh(R.id.swipeRefreshView) 

   // 2 
   override val presenter by lazy 

   { MainPresenter(this, MarvelRepository.get()) } // 3 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       requestWindowFeature(Window.FEATURE_NO_TITLE) 
       setContentView(R.layout.activity_main) 
       recyclerView.layoutManager = GridLayoutManager(this, 2) 
       swipeRefreshView.setOnRefreshListener 

       { presenter.onRefresh() } // 4 
       presenter.onViewCreated() // 4 
   } 

   override fun show(items: List<MarvelCharacter>) { 
       val categoryItemAdapters = items.map(::CharacterItemAdapter) 
       recyclerView.adapter = MainListAdapter(categoryItemAdapters) 
   } 

   override fun showError(error: Throwable) { 
       toast("Error: ${error.message}") // 2 
       error.printStackTrace() 
   } 
} 
  1. Activity 应该扩展BaseActivityWithPresenter并实现MainView

  2. bindToSwipeRefreshtoast还没有实现。

  3. 我们使 Presenter 懒惰。第一个参数是指向MainView接口后面的活动的引用。

  4. 我们需要使用它的方法将事件传递给 Presenter。

在前面的代码中,我们使用了两个已在书中描述的函数,toast用于在屏幕上显示提示,bindToSwipeRefresh用于绑定滑动刷新的可见性属性:

// ViewExt.kt 
package com.sample.marvelgallery.view.common 

import android.app.Activity 
import android.content.Context 
import android.support.annotation.IdRes 
import android.support.v4.widget.SwipeRefreshLayout 
import android.support.v7.widget.RecyclerView 
import android.view.View 
import android.widget.ImageView 
import android.widget.Toast 
import com.bumptech.glide.Glide 
import kotlin.properties.ReadWriteProperty 
import kotlin.reflect.KProperty 

fun <T : View> RecyclerView.ViewHolder.bindView(viewId: Int) 
       = lazy { itemView.findViewById<T>(viewId) } 

fun ImageView.loadImage(photoUrl: String) { 
   Glide.with(context) 
           .load(photoUrl) 
           .into(this) 
} 

fun Context.toast(text: String, length: Int = Toast.LENGTH_LONG) { 
   Toast.makeText(this, text, length).show() 
} 

fun Activity.bindToSwipeRefresh(@IdRes swipeRefreshLayoutId: Int): ReadWriteProperty<Any?, Boolean> 
       = SwipeRefreshBinding(lazy { findViewById<SwipeRefreshLayout>(swipeRefreshLayoutId) }) 

private class SwipeRefreshBinding(lazyViewProvider: Lazy<SwipeRefreshLayout>) : ReadWriteProperty<Any?, Boolean> { 

   val view by lazyViewProvider 

   override fun getValue(thisRef: Any?, 

   property: KProperty<*>): Boolean { 
       return view.isRefreshing 
   } 

   override fun setValue(thisRef: Any?, 

   property: KProperty<*>, value: Boolean) { 
       view.isRefreshing = value 
   } 
} 

现在我们的应用程序应该正确显示角色列表:

我们的第一个用例已经实现。我们可以继续下一个。

角色搜索

我们需要实现的另一个行为是角色搜索。以下是用例定义,启动应用程序后,用户可以通过角色名称搜索角色。

为了添加它,我们将在activity_main布局中添加EditText

<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout  

   android:id="@+id/charactersView" 
   android:layout_width="match_parent" 
   android:layout_height="match_parent" 
   android:background="@android:color/white" 
   android:fitsSystemWindows="true"> 

<!-- Dummy item to prevent EditText from receiving 

     focus on initial load --> 
   <LinearLayout 
       android:layout_width="0px" 
       android:layout_height="0px" 
       android:focusable="true" 
       android:focusableInTouchMode="true" 
       tools:ignore="UselessLeaf" /> 

  <android.support.design.widget.TextInputLayout 
     android:id="@+id/searchViewLayout" 
     android:layout_width="match_parent" 
     android:layout_height="wrap_content" 
     android:layout_margin="@dimen/element_padding"> 

     <EditText 
         android:id="@+id/searchView" 
         android:layout_width="match_parent" 
         android:layout_height="wrap_content" 
         android:layout_centerHorizontal="true" 
         android:hint="@string/search_hint" /> 

  </android.support.design.widget.TextInputLayout> 

   <android.support.v4.widget.SwipeRefreshLayout  
       android:id="@+id/swipeRefreshView" 
       android:layout_width="match_parent" 
       android:layout_height="match_parent" 
       android:layout_below="@+id/searchViewLayout" 
       app:layout_behavior="@string/appbar_scrolling_view_behavior"> 

       <android.support.v7.widget.RecyclerView 
           android:id="@+id/recyclerView" 
           android:layout_width="match_parent" 
           android:layout_height="match_parent" 
           android:scrollbars="vertical" /> 

   </android.support.v4.widget.SwipeRefreshLayout> 

   <TextView 
       android:layout_width="match_parent" 
       android:layout_height="wrap_content" 
       android:layout_alignParentBottom="true" 
       android:background="@android:color/white" 
       android:gravity="center" 
       android:text="@string/marvel_copyright_notice" /> 
</RelativeLayout> 

我们需要添加Android Support Design库依赖,以允许使用TextInputLayout

implementation "com.android.support:appcompat-v7:$android_support_version" 
implementation "com.android.support:design:$android_support_version" 
implementation "com.android.support:recyclerview-v7:$android_support_version" 

strings.xml中定义了search_hint字符串:

<resources> 
   <string name="app_name">MarvelGallery</string> 
   <string name="search_hint">Search for character</string> 
   <string name="marvel_copyright_notice">

      Data provided by Marvel. © 2017 MARVEL

   </string> 
</resources> 

此外,为了在键盘打开时保持通知有关 Marvel 版权的标签,我们还需要在AndroidManifest中的activity定义中将adjustResize设置为windowSoftInputMode

<activity 
   android:name="com.sample.marvelgallery.view.main.MainActivity" 
   android:windowSoftInputMode="adjustResize"> 
   <intent-filter> 
       <action android:name="android.intent.action.MAIN" /> 
       <category android:name="android.intent.category.LAUNCHER" /> 
   </intent-filter> 
</activity> 

我们应该看到以下预览:

现在我们在MainActivity中添加了一个搜索字段:

我们期望的行为是,每当用户更改搜索字段中的文本时,将加载新列表。我们需要在MainPresenter中添加一个新方法,用于通知 Presenter 文本已更改。我们将称之为onSearchChanged

fun onRefresh() { 
   loadCharacters() 
} 

fun onSearchChanged(text: String) { 
   // TODO 
}

private fun loadCharacters() {

   subscriptions += repository.getAllCharacters()

           .applySchedulers()

           .doOnSubscribe { view.refresh = true }

           .doFinally { view.refresh = false }

           .subscribeBy(

               onSuccess = view::show,

               onError = view::showError

         )

   }

}

我们需要更改MarvelRepository的定义,以接受搜索查询作为getAllCharacters参数(记得也更新BaseMarvelRepository):

interface MarvelRepository { 

   fun getAllCharacters(searchQuery: String?): 

   Single<List<MarvelCharacter>> 

   companion object : Provider<MarvelRepository>() { 
       override fun creator() = MarvelRepositoryImpl() 
   } 
} 

因此,我们必须更新实现:

class MarvelRepositoryImpl : MarvelRepository { 

   val api = retrofit.create(MarvelApi::class.java) 

   override fun getAllCharacters(searchQuery: String?): 

   Single<List<MarvelCharacter>> = api.getCharacters( 
           offset = 0, 
           searchQuery = searchQuery, 
           limit = elementsOnListLimit 
   ).map { it.data?.results.orEmpty().map(::MarvelCharacter) ?: 

    emptyList() } 

   companion object { 
       const val elementsOnListLimit = 50 
   } 
} 

我们还需要更新网络请求的定义:

interface MarvelApi { 

   @GET("characters") 
   fun getCharacters( 
           @Query("offset") offset: Int?, 
           @Query("nameStartsWith") searchQuery: String?, 
           @Query("limit") limit: Int? 
   ): Single<DataWrapper<List<CharacterMarvelDto>>> 
} 

为了允许代码编译,我们需要在MainPresenter中提供null作为getAllCharacters参数:

private fun loadCharacters() {

   subscriptions += repository.getAllCharacters(null)

           .applySchedulers()

           .doOnSubscribe { view.refresh = true }

           .doFinally { view.refresh = false }

           .subscribeBy(

                       onSuccess = view::show,

                       onError = view::showError

         )

   }

}

我们需要更新BaseMarvelRepository

package com.sample.marvelgallery.helpers 

import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import io.reactivex.Single 

class BaseMarvelRepository( 
       val onGetCharacters: (String?) -> Single<List<MarvelCharacter>> 
) : MarvelRepository { 

   override fun getAllCharacters(searchQuery: String?) 
           = onGetCharacters(searchQuery) 
} 

现在我们的网络实现返回一个从查询开始的角色列表,或者如果我们没有指定任何查询,则返回一个填充列表。是时候实现 Presenter 了。让我们定义以下测试:

@file:Suppress("IllegalIdentifier") 

package com.sample.marvelgallery 

import com.sample.marvelgallery.helpers.BaseMainView 
import com.sample.marvelgallery.helpers.BaseMarvelRepository 
import com.sample.marvelgallery.presenter.MainPresenter 
import io.reactivex.Single 
import org.junit.Assert.* 
import org.junit.Test 

class MainPresenterSearchTest { 

   @Test 
   fun `When view is created, then search query is null`() { 
       assertOnAction { onViewCreated() } searchQueryIsEqualTo null 
   } 

   @Test 
   fun `When text is changed, then we are searching for new query`() { 
       for (text in listOf("KKO", "HJ HJ", "And so what?")) 
           assertOnAction { onSearchChanged(text) } 

           searchQueryIsEqualTo text 
   } 

   private fun assertOnAction(action: MainPresenter.() -> Unit)  
         = PresenterActionAssertion(action) 

   private class PresenterActionAssertion(val actionOnPresenter: 

       MainPresenter.() -> Unit) { 

       infix fun searchQueryIsEqualTo(expectedQuery: String?) { 
           var checkApplied = false 
           val view = BaseMainView(onShowError = { fail() }) 
           val marvelRepository = BaseMarvelRepository { searchQuery -> 
               assertEquals(expectedQuery, searchQuery) 
               checkApplied = true 
               Single.never() 
           } 
           val mainPresenter = MainPresenter(view, marvelRepository) 
           mainPresenter.actionOnPresenter() 
           assertTrue(checkApplied) 
       } 
   } 
} 

为了使以下测试通过,我们需要将搜索查询作为MainPresenterloadCharacters方法的参数添加默认参数:

fun onSearchChanged(text: String) { 
   loadCharacters(text) 
} 

private fun loadCharacters(searchQuery: String? = null) {

   subscriptions += repository.getAllCharacters(searchQuery)

           .applySchedulers()

           .doOnSubscribe { view.refresh = true }

           .doFinally { view.refresh = false }

           .subscribeBy(

                       onSuccess = view::show,

                       onError = view::showError

         )

   }

}

但棘手的部分是 Marvel API 不允许将空格作为搜索查询。应该发送一个null。因此,如果用户删除最后一个字符,或者尝试在搜索字段中只放置空格,那么应用程序将崩溃。我们应该防止这种情况发生。这是一个测试,检查 Presenter 是否将只有空格的查询更改为null

@Test 
fun `When text is changed, then we are searching for new query`() { 
   for (text in listOf("KKO", "HJ HJ", "And so what?")) 
       assertOnAction { onSearchChanged(text) } 

       searchQueryIsEqualTo text 
} 

@Test 
fun `For blank text, there is request with null query`() { 
   for (emptyText in listOf("", "   ", "       ")) 
       assertOnAction { onSearchChanged(emptyText) } 

       searchQueryIsEqualTo null 
} 

private fun assertOnAction(action: MainPresenter.() -> Unit)  
      = PresenterActionAssertion(action) 

We can implement a security mechanism in the loadCharacters method: 

private fun loadCharacters(searchQuery: String? = null) { 
   val qualifiedSearchQuery = if (searchQuery.isNullOrBlank()) null 

                              else searchQuery 
   subscriptions += repository 
           .getAllCharacters(qualifiedSearchQuery) 
           .applySchedulers() 
           .smartSubscribe( 
                   onStart = { view.refresh = true }, 
                   onSuccess = view::show, 
                   onError = view::showError, 
                   onFinish = { view.refresh = false } 
           ) 
} 

现在所有的测试都通过了:

我们仍然需要实现一个Activity功能,当文本发生变化时将调用 Presenter。我们将使用第七章中定义的可选回调类来实现:

// TextChangedListener.kt 
package com.sample.marvelgallery.view.common 

import android.text.Editable 
import android.text.TextWatcher 
import android.widget.TextView 

fun TextView.addOnTextChangedListener(config: TextWatcherConfiguration.() -> Unit) { 
   addTextChangedListener(TextWatcherConfiguration().apply { config() }
   addTextChangedListener(textWatcher) 
} 

class TextWatcherConfiguration : TextWatcher { 

   private var beforeTextChangedCallback: 

   (BeforeTextChangedFunction)? = null 
   private var onTextChangedCallback: 

   (OnTextChangedFunction)? = null 
   private var afterTextChangedCallback: 

   (AfterTextChangedFunction)? = null 

   fun beforeTextChanged(callback: BeforeTextChangedFunction) { 
       beforeTextChangedCallback = callback 
   } 

   fun onTextChanged(callback: OnTextChangedFunction) { 
       onTextChangedCallback = callback 
   } 

   fun afterTextChanged(callback: AfterTextChangedFunction) { 
       afterTextChangedCallback = callback 
   } 

   override fun beforeTextChanged(s: CharSequence, 

   start: Int, count: Int, after: Int) { 
       beforeTextChangedCallback?.invoke(s.toString(), 

       start, count, after) 
   } 

   override fun onTextChanged(s: CharSequence, start: Int, 

   before: Int, count: Int) { 
       onTextChangedCallback?.invoke(s.toString(), 

       start, before, count) 
   } 

   override fun afterTextChanged(s: Editable) { 
       afterTextChangedCallback?.invoke(s) 
   } 
} 

private typealias BeforeTextChangedFunction = 

  (text: String, start: Int, count: Int, after: Int) -> Unit 
private typealias OnTextChangedFunction = 

  (text: String, start: Int, before: Int, count: Int) -> Unit 
private typealias AfterTextChangedFunction = 

  (s: Editable) -> Unit 

并在MainActivityonCreate方法中使用它:

package com.sample.marvelgallery.view.main 

import android.os.Bundle 
import android.support.v7.widget.GridLayoutManager 
import android.view.Window 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import com.sample.marvelgallery.view.common.BaseActivityWithPresenter 
import com.sample.marvelgallery.view.common.addOnTextChangedListener 
import com.sample.marvelgallery.view.common.bindToSwipeRefresh 
import com.sample.marvelgallery.view.common.toast 
import kotlinx.android.synthetic.main.activity_main.* 

class MainActivity : BaseActivityWithPresenter(), MainView { 

   override var refresh by bindToSwipeRefresh(R.id.swipeRefreshView) 
   override val presenter by lazy 

     { MainPresenter(this, MarvelRepository.get()) } 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       requestWindowFeature(Window.FEATURE_NO_TITLE) 
       setContentView(R.layout.activity_main) 
       recyclerView.layoutManager = GridLayoutManager(this, 2) 
       swipeRefreshView.setOnRefreshListener { presenter.onRefresh() } 
       searchView.addOnTextChangedListener { 
           onTextChanged { text, _, _, _ -> 
               presenter.onSearchChanged(text) 
           } 
       } 
       presenter.onViewCreated() 
   } 

   override fun show(items: List<MarvelCharacter>) { 
       val categoryItemAdapters = items.map(::CharacterItemAdapter) 
       recyclerView.adapter = MainListAdapter(categoryItemAdapters) 
   } 

   override fun showError(error: Throwable) { 
       toast("Error: ${error.message}") 
       error.printStackTrace() 
   } 
} 

这就是我们需要定义角色搜索功能的全部内容。现在我们可以构建应用程序并使用它来查找我们喜欢的角色:

有了一个正确工作的应用程序,我们可以继续下一个用例。

角色概要显示

仅仅通过角色搜索是不够的。为了使应用程序功能正常,我们应该添加角色描述显示。这是我们定义的用例--当用户点击某个角色图片时,会显示一个概要。角色概要包含角色名称、照片、描述和出现次数。

要实现这个用例,我们需要创建一个新的活动和布局,来定义这个Activity的外观。为此,在com.sample.marvelgallery.view.character包中创建一个名为CharacterProfileActivity的新 Activity:

我们将从布局更改(在activity_character_profile.xml中)开始实现它。这是我们想要实现的最终结果:

基本元素是CoordinatorLayout,其中AppBarCollapsingToolbarLayout都用于实现材料设计中的折叠效果:

逐步实现折叠效果。

我们还需要用于描述和出现次数的TextView,这些将在下一个用例中填充数据。这是完整的activity_character_profile布局定义:

<?xml version="1.0" encoding="utf-8"?> 
<android.support.design.widget.CoordinatorLayout  

   android:id="@+id/character_detail_layout" 
   android:layout_width="match_parent" 
   android:layout_height="match_parent" 
   android:background="@android:color/white"> 

   <android.support.design.widget.AppBarLayout 
       android:id="@+id/appBarLayout" 
       android:layout_width="match_parent" 
       android:layout_height="wrap_content" 
       android:theme="@style/ThemeOverlay.AppCompat.ActionBar"> 

       <android.support.design.widget.CollapsingToolbarLayout 
           android:id="@+id/toolbarLayout" 
           android:layout_width="match_parent" 
           android:layout_height="match_parent" 
           app:contentScrim="?attr/colorPrimary" 
           app:expandedTitleTextAppearance="@style/ItemTitleName" 
           app:layout_scrollFlags="scroll|exitUntilCollapsed"> 

           <android.support.v7.widget.AppCompatImageView 
               android:id="@+id/headerView" 
               android:layout_width="match_parent" 
               android:layout_height="@dimen/character_header_height" 
               android:background="@color/colorPrimaryDark" 
               app:layout_collapseMode="parallax" /> 

           <android.support.v7.widget.Toolbar 
               android:id="@+id/toolbar" 
               android:layout_width="match_parent" 
               android:layout_height="?attr/actionBarSize" 
               android:background="@android:color/transparent" 
               app:layout_collapseMode="pin" 
               app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> 

       </android.support.design.widget.CollapsingToolbarLayout> 
   </android.support.design.widget.AppBarLayout> 

   <android.support.v4.widget.NestedScrollView 
       android:layout_width="match_parent" 
       android:layout_height="match_parent" 
       android:overScrollMode="never" 
       app:layout_behavior="@string/appbar_scrolling_view_behavior"> 

       <LinearLayout 
           android:id="@+id/details_content_frame" 
           android:layout_width="match_parent" 
           android:layout_height="match_parent" 
           android:focusableInTouchMode="true" 
           android:orientation="vertical"> 

           <TextView 
               android:id="@+id/descriptionView" 
               android:layout_width="match_parent" 
               android:layout_height="wrap_content" 
               android:gravity="center" 
               android:padding="@dimen/character_description_padding" 
               android:textSize="@dimen/standard_text_size" 
               tools:text="This is some long text that will be visible as an character description." /> 

           <TextView 
               android:id="@+id/occurrencesView" 
               android:layout_width="match_parent" 
               android:layout_height="wrap_content" 
               android:padding="@dimen/character_description_padding" 
               android:textSize="@dimen/standard_text_size" 
               tools:text="He was in following comics:\n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO \n* KOKOKO " /> 
       </LinearLayout> 

   </android.support.v4.widget.NestedScrollView> 

   <TextView 
       android:layout_width="match_parent" 
       android:layout_height="wrap_content" 
       android:layout_gravity="bottom" 
       android:background="@android:color/white" 
       android:gravity="bottom|center" 
       android:text="@string/marvel_copyright_notice" /> 

   <ProgressBar 
       android:id="@+id/progressView" 
       style="?android:attr/progressBarStyleLarge" 
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content" 
       android:layout_gravity="center" 
       android:visibility="gone" /> 

</android.support.design.widget.CoordinatorLayout> 

我们还需要在styles.xml中添加以下样式:

<resources> 

   <!-- Base application theme. --> 
   <style name="AppTheme" 

          parent="Theme.AppCompat.Light.DarkActionBar"> 
       <!-- Customize your theme here. --> 
       <item name="colorPrimary">@color/colorPrimary</item> 
       <item name="colorPrimaryDark">@color/colorPrimaryDark</item> 
       <item name="colorAccent">@color/colorAccent</item> 
   </style> 
   <style name="AppFullScreenTheme" 

          parent="Theme.AppCompat.Light.NoActionBar"> 
       <item name="android:windowNoTitle">true</item> 
       <item name="android:windowActionBar">false</item> 
       <item name="android:windowFullscreen">true</item> 
       <item name="android:windowContentOverlay">@null</item> 
   </style> 

   <style name="ItemTitleName" 

          parent="TextAppearance.AppCompat.Headline"> 
       <item name="android:textColor">@android:color/white</item> 
       <item name="android:shadowColor">@color/colorPrimaryDark</item> 
       <item name="android:shadowRadius">3.0</item> 
   </style> 

   <style name="ItemDetailTitle" 

          parent="@style/TextAppearance.AppCompat.Small"> 
       <item name="android:textColor">@color/colorAccent</item> 
   </style> 

</resources> 

我们需要在AndroidManifest中将AppFullScreenTheme定义为CharacterProfileActivity的主题:

<activity android:name=".view.CharacterProfileActivity" 
   android:theme="@style/AppFullScreenTheme" /> 

这是定义的布局的预览:

这个视图将用于显示有关角色的数据,但首先我们需要从MainActivity中打开它。我们需要在CharacterItemAdapter中设置onClickListener,它调用构造函数提供的clicked回调:

package com.sample.marvelgallery.view.main 

import android.support.v7.widget.RecyclerView 
import android.view.View 
import android.widget.ImageView 
import android.widget.TextView 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.view.common.ItemAdapter 
import com.sample.marvelgallery.view.common.bindView 
import com.sample.marvelgallery.view.common.loadImage 

class CharacterItemAdapter( 
       val character: MarvelCharacter, 
       val clicked: (MarvelCharacter) -> Unit 
) : ItemAdapter<CharacterItemAdapter.ViewHolder>(R.layout.item_character) { 

   override fun onCreateViewHolder(itemView: View) = 

   ViewHolder(itemView) 

   override fun ViewHolder.onBindViewHolder() { 
       textView.text = character.name 
       imageView.loadImage(character.imageUrl) 
       itemView.setOnClickListener { clicked(character) } 
   } 

   class ViewHolder(itemView: View) : 

   RecyclerView.ViewHolder(itemView) { 
       val textView by bindView<TextView>(R.id.textView) 
       val imageView by bindView<ImageView>(R.id.imageView) 
   } 
} 

我们需要更新MainActivity

package com.sample.marvelgallery.view.main 

import android.os.Bundle 
import android.support.v7.widget.GridLayoutManager 
import android.view.Window 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.data.MarvelRepository 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.presenter.MainPresenter 
import com.sample.marvelgallery.view.character.CharacterProfileActivity 
import com.sample.marvelgallery.view.common.BaseActivityWithPresenter 
import com.sample.marvelgallery.view.common.addOnTextChangedListener 
import com.sample.marvelgallery.view.common.bindToSwipeRefresh 
import com.sample.marvelgallery.view.common.toast 
import kotlinx.android.synthetic.main.activity_main.* 

class MainActivity : BaseActivityWithPresenter(), MainView { 

   override var refresh by bindToSwipeRefresh(R.id.swipeRefreshView) 
   override val presenter by lazy

   { MainPresenter(this, MarvelRepository.get()) } 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       requestWindowFeature(Window.FEATURE_NO_TITLE) 
       setContentView(R.layout.activity_main) 
       recyclerView.layoutManager = GridLayoutManager(this, 2) 
       swipeRefreshView.setOnRefreshListener { presenter.onRefresh() } 
       searchView.addOnTextChangedListener { 
           onTextChanged { text, _, _, _ -> 
               presenter.onSearchChanged(text) 
           } 
       } 
       presenter.onViewCreated() 
   } 

   override fun show(items: List<MarvelCharacter>) { 
       val categoryItemAdapters = 

       items.map(this::createCategoryItemAdapter) 
       recyclerView.adapter = MainListAdapter(categoryItemAdapters) 
   } 

   override fun showError(error: Throwable) { 
       toast("Error: ${error.message}") 
       error.printStackTrace() 
   } 

   private fun createCategoryItemAdapter(character: MarvelCharacter) 
           = CharacterItemAdapter(character, 

             { showHeroProfile(character) }) 

   private fun showHeroProfile(character: MarvelCharacter) { 
       CharacterProfileActivity.start(this, character) 
   } 
} 

在前面的实现中,我们使用了CharacterProfileActivity伴生对象中的一个方法来启动CharacterProfileActivity。我们需要将MarvelCharacter对象传递给这个方法。传递MarvelCharacter对象的最有效方式是将其作为parcelable传递。为了允许这样做,MarvelCharacter必须实现Parcelable接口。这就是为什么一个有用的解决方案是使用一些注解处理库,如ParcelerPaperParcelSmuggler,来生成必要的元素。我们将使用项目中已经存在的 Kotlin Android 扩展解决方案。在书籍出版时,它仍然是实验性的,因此需要在build.gradle模块中添加以下定义:

androidExtensions {

   experimental = true

}

我们需要在类之前添加Parcelize注解,并且需要使这个类实现Parcelable。我们还需要添加错误抑制,以隐藏默认的 Android 警告:

package com.sample.marvelgallery.model 

import android.annotation.SuppressLint 
import android.os.Parcelable 
import com.sample.marvelgallery.data.network.dto.CharacterMarvelDto 

import kotlinx.android.parcel.Parcelize

@SuppressLint("ParcelCreator")

@Parcelize

   constructor(dto: CharacterMarvelDto) : this( 
           name = dto.name, 
           imageUrl = dto.imageUrl 
   )
} 

现在我们可以实现start函数和character字段,它将使用属性委托从 Intent 中获取参数值:

package com.sample.marvelgallery.view.character 

import android.content.Context 
import android.support.v7.app.AppCompatActivity 
import android.os.Bundle 
import android.view.MenuItem 
import com.sample.marvelgallery.R 
import com.sample.marvelgallery.model.MarvelCharacter 
import com.sample.marvelgallery.view.common.extra 
import com.sample.marvelgallery.view.common.getIntent 
import com.sample.marvelgallery.view.common.loadImage 
import kotlinx.android.synthetic.main.activity_character_profile.* 

class CharacterProfileActivity : AppCompatActivity() { 

   val character: MarvelCharacter by extra(CHARACTER_ARG) // 1 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       setContentView(R.layout.activity_character_profile) 
       setUpToolbar() 
       supportActionBar?.title = character.name 
       headerView.loadImage(character.imageUrl, centerCropped = true) // 1 
   } 

   override fun onOptionsItemSelected(item: MenuItem): Boolean = when { 
       item.itemId == android.R.id.home -> onBackPressed().let { true } 
       else -> super.onOptionsItemSelected(item) 
   } 

   private fun setUpToolbar() { 
       setSupportActionBar(toolbar) 
       supportActionBar?.setDisplayHomeAsUpEnabled(true) 
   } 

   companion object { 

       private const val CHARACTER_ARG = "com.sample.marvelgallery.view.character.CharacterProfileActivity.CharacterArgKey" 

       fun start(context: Context, character: MarvelCharacter) { 
           val intent = context 
                   .getIntent<CharacterProfileActivity>() // 1 
                   .apply { putExtra(CHARACTER_ARG, character) } 
           context.startActivity(intent) 
       } 
   } 
} 
  1. extragetIntent扩展函数已经在书中介绍过,但在项目中尚未实现。此外,loadImage将显示错误,因为它需要更改。

我们需要更新loadImage,并将extragetIntent定义为顶级函数:

// ViewExt.kt 
package com.sample.marvelgallery.view.common 

import android.app.Activity 
import android.content.Context 
import android.content.Intent 
import android.os.Parcelable 
import android.support.annotation.IdRes 
import android.support.v4.widget.SwipeRefreshLayout 
import android.widget.ImageView 
import android.widget.Toast 
import com.bumptech.glide.Glide 
import kotlin.properties.ReadWriteProperty 
import kotlin.reflect.KProperty 
import android.support.v7.widget.RecyclerView 
import android.view.View 

fun <T : View> RecyclerView.ViewHolder.bindView(viewId: Int)  
      = lazy { itemView.findViewById<T>(viewId) } 

fun ImageView.loadImage(photoUrl: String, centerCropped: Boolean = false) { 
   Glide.with(context) 
           .load(photoUrl) 
           .apply { if (centerCropped) centerCrop() } 
           .into(this) 
} 

fun <T : Parcelable> Activity.extra(key: String, default: T? = null): Lazy<T>  
      = lazy { intent?.extras?.getParcelable<T>(key) ?: default ?: throw Error("No value $key in extras") } 

inline fun <reified T : Activity> Context.getIntent() = Intent(this, T::class.java) 

// ...

我们可以使用一些库来生成这些方法,而不是定义启动 Activity 的函数。例如,我们可以使用ActivityStarter库。这就是CharacterProfileActivity将会是什么样子:

class CharacterProfileActivity : AppCompatActivity() { 

   @get:Arg val character: MarvelCharacter by argExtra() 

   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       setContentView(R.layout.activity_character_profile) 
       setUpToolbar() 
       supportActionBar?.title = character.name 
       headerView.loadImage(character.imageUrl, centerCropped = true) // 1 
   } 

   override fun onOptionsItemSelected(item: MenuItem): Boolean = when { 
       item.itemId == android.R.id.home -> onBackPressed().let { true } 
       else -> super.onOptionsItemSelected(item) 
   } 

   private fun setUpToolbar() { 
       setSupportActionBar(toolbar) 
       supportActionBar?.setDisplayHomeAsUpEnabled(true) 
   } 
} 

我们应该启动它或使用生成的类CharacterProfileActivityStarter的静态方法获取其 Intent:

CharacterProfileActivityStarter.start(context, character) 
val intent = CharacterProfileActivityStarter.getIntent(context, character) 

为了允许它,我们需要在模块build.gradle中使用kapt插件(用于支持 Kotlin 中的注解处理):

apply plugin: 'kotlin-kapt' 

build.gradle模块中的ActivityStarter依赖项:

implementation 'com.github.marcinmoskala.activitystarter:activitystarter:1.00' 
implementation 'com.github.marcinmoskala.activitystarter:activitystarter-kotlin:1.00' 
kapt 'com.github.marcinmoskala.activitystarter:activitystarter-compiler:1.00' 

经过这些更改,当我们点击MainActivity中的角色时,CharacterProfileActivity将会启动:

我们正在显示名称并展示角色照片。下一步是显示描述和事件列表。所需的数据可以在 Marvel API 中找到,我们只需要扩展 DTO 模型来获取它们。我们需要添加ListWrapper来保存列表:

package com.sample.marvelgallery.data.network.dto 

class ListWrapper<T> { 
   var items: List<T> = listOf() 
} 

我们需要定义ComicDto,其中包含有关事件发生的数据:

package com.sample.marvelgallery.data.network.dto 

class ComicDto { 
   lateinit var name: String 
} 

我们需要更新CharacterMarvelDto

package com.sample.marvelgallery.data.network.dto 

class CharacterMarvelDto { 

   lateinit var name: String 
   lateinit var description: String 
   lateinit var thumbnail: ImageDto 
   var comics: ListWrapper<ComicDto> = ListWrapper() 
   var series: ListWrapper<ComicDto> = ListWrapper() 
   var stories: ListWrapper<ComicDto> = ListWrapper() 
   var events: ListWrapper<ComicDto> = ListWrapper() 

   val imageUrl: String 
       get() = thumbnail.completeImagePath 
} 

现在从 API 中读取数据并保存在 DTO 对象中,但为了在项目中使用它们,我们还需要更改MarvelCharacter类的定义,并添加一个新的构造函数:

@SuppressLint("ParcelCreator")

@Parcelize

class MarvelCharacter( 
       val name: String, 
       val imageUrl: String, 
       val description: String, 
       val comics: List<String>, 
       val series: List<String>, 
       val stories: List<String>, 
       val events: List<String> 
) : Parcelable { 

   constructor(dto: CharacterMarvelDto) : this( 
           name = dto.name, 
           imageUrl = dto.imageUrl, 
           description = dto.description, 
           comics = dto.comics.items.map { it.name }, 
           series = dto.series.items.map { it.name }, 
           stories = dto.stories.items.map { it.name }, 
           events = dto.events.items.map { it.name } 
   ) 
} 

现在我们可以更新CharacterProfileActivity来显示描述和事件列表:

class CharacterProfileActivity : AppCompatActivity() { 

   val character: MarvelCharacter by extra(CHARACTER_ARG) 
   override fun onCreate(savedInstanceState: Bundle?) { 
       super.onCreate(savedInstanceState) 
       setContentView(R.layout.activity_character_profile) 
       setUpToolbar() 
       supportActionBar?.title = character.name 
       descriptionView.text = character.description 
       occurrencesView.text = makeOccurrencesText() // 1 
       headerView.loadImage(character.imageUrl, centerCropped = true) 
   } 

   override fun onOptionsItemSelected(item: MenuItem): Boolean = when { 
       item.itemId == android.R.id.home -> onBackPressed().let { true } 
       else -> super.onOptionsItemSelected(item) 
   } 

   private fun setUpToolbar() { 
       setSupportActionBar(toolbar) 
       supportActionBar?.setDisplayHomeAsUpEnabled(true) 
   } 

   private fun makeOccurrencesText(): String = "" // 1, 2 
           .addList(R.string.occurrences_comics_list_introduction, character.comics) 
           .addList(R.string.occurrences_series_list_introduction, character.series) 
           .addList(R.string.occurrences_stories_list_introduction, character.stories) 
           .addList(R.string.occurrences_events_list_introduction, character.events) 

   private fun String.addList(introductionTextId: Int, list: List<String>): String { // 3 
       if (list.isEmpty()) return this 
       val introductionText = getString(introductionTextId) 
       val listText = list.joinToString(transform = 

           { " $bullet $it" }, separator = "\n") 
       return this + "$introductionText\n$listText\n\n" 
   } 

   companion object { 
       private const val bullet = '\u2022' // 4 
       private const val CHARACTER_ARG = "com.naxtlevelofandroiddevelopment.marvelgallery.presentation.heroprofile.CharacterArgKey" 

       fun start(context: Context, character: MarvelCharacter) { 
           val intent = context 
                   .getIntent<CharacterProfileActivity>() 
                   .apply { putExtra(CHARACTER_ARG, character) } 
           context.startActivity(intent) 
       } 
   } 
}
  1. 出现列表的组合是一个相当复杂的任务,因此我们将其提取到函数makeOccurrencesText中。在那里,对于每种出现类型(漫画、系列等),我们希望在有这种类型的出现时显示介绍文本和列表。我们还希望在每个项目前加上一个项目符号。

  2. makeOccurrencesText是一个单表达式函数,它使用addList来将初始空字符串附加上我们想要显示的下一个列表。

  3. addList是一个成员扩展函数。如果提供的列表为空,则返回一个未更改的字符串,或者返回一个附加了介绍文本和带有项目列表的字符串。

  4. 这是用作列表项目符号的角色。

我们还需要在strings.xml中定义字符串:

<resources> 
   <string name="app_name">Marvel Gallery</string> 
   <string name="marvel_copyright_notice">

       Data provided by Marvel. © 2017 MARVEL</string> 
   <string name="search_hint">Search for character</string> 
   <string name="occurrences_comics_list_introduction">Comics:</string> 
   <string name="occurrences_series_list_introduction">Series:</string> 
   <string name="occurrences_stories_list_introduction">Stories:</string> 
   <string name="occurrences_events_list_introduction">Events:</string> 
</resources> 

现在我们可以看到整个角色资料--角色名称、图片、描述以及在漫画、系列、事件和故事中的出现列表:

摘要

应用程序已经完成,但仍然可以添加许多功能。在这个应用程序中,我们看到了 Kotlin 如何简化 Android 开发的一些示例。但仍然有很多解决方案等待发现。Kotlin 简化了 Android 开发的任何层次--从常见操作,如监听器设置或视图元素引用,到高级功能,如函数式编程或集合处理。

这本书无法涵盖关于 Kotlin 的 Android 开发的所有内容。它旨在展示足够的内容,以便每个人都可以开始自己的冒险,拥有充满想法和功能理解的行囊。下一步是打开 Android Studio,创建自己的项目,并开始享受 Kotlin 带来的乐趣。大冒险就在你面前。

posted @ 2024-05-22 15:09  绝不原创的飞龙  阅读(21)  评论(0编辑  收藏  举报