Kotlin-安卓开发学习手册-四-

118 阅读1小时+

Kotlin 安卓开发学习手册(四)

原文:Learn Kotlin for Android Development

协议:CC BY-NC-SA 4.0

十一、处理相等性

同一性相等性之间有着明显的区别。如果两个事物实际上是相同的,那么它们就是相同的。如果你今天早上买了一支白蜡烛,姑且称之为 A,你购物袋里的白蜡烛和今天下午放在烛台上的白蜡烛是一样的,因此完全相同(假设这是你拥有的唯一一支蜡烛)。现在假设你从同一个制造商那里买了第二支相同型号的蜡烛 B。除了你有时会听到的一些语言错误,这两根蜡烛是相同的。蜡烛 A 和 B 不一样,但是等于。这是因为它们具有相同的特征:相同的颜色、相同的重量、相同的直径和相同的长度。但是,打住:这不一定是真的。制造商称这种蜡烛重 300 克,但是高精度天平告诉我们蜡烛 A 重 300.00245 克,蜡烛 B 重 299.99734 克,但是如果你用厨房秤,蜡烛 A 和 B 的重量是一样的。因此,你可以看到,相等性取决于严格,它是相对的。

同一性和相等性之间的比较给我们上了重要的一课:同一性适用于相同的事物,而相等性是相对的,取决于某种定义。

Kotlin 的同一性

在 Kotlin 中有一个恒等运算符===和它的反义词!==。在 Kotlin,同一性代表参照同一性,这意味着如果两个变量指向同一个对象,或者引用同一个对象,它们被认为是相同的:

data class A(val x:Double)
val a = A(7.0)
val b = A(7.0)
val c = a
val aIdenticalToC = a === c // -> true
val aIdenticalToB = a === b // -> false

实际上,您可能不会经常使用标识。在大多数情况下,让不同的变量指向同一个对象无论如何都不是好的编码风格,此外,同一性对于不同的程序流来说没有太大的不同。最后,尽管两个对象中的所有属性都具有相同的值,但是这两个对象的比较结果都为 false,这是令人困惑的,并且会影响代码的可读性。因此,同一性检查的实际用途是有限的。

注意

在数据库环境中,还有另一个同一性的概念。那里通常有一个用于数据记录的数字 ID 字段。这个字段被用作相应对象标识的代理,而不是语言的引用标识===。在本章中,我们不讨论这种数据库类型的同一性。

Kotlin 的相等性

对于等式,Kotlin 提供了比较运算符==,以及它的反义词!=。除了标识之外,一个对象必须告诉它是否等于其他对象。如果不显式地这样做,将使用相等检查的基本实现,这又回到了同一性检查。

对数字、布尔值、字符和字符串的相等性检查做了显而易见的事情:如果字符串包含完全相同的字符,则它们相等;如果字符包含相同的字母,则它们相等;如果数字和布尔值具有相同的值,则它们相等。

等于和哈希代码

类处理相等性检查的方式由两个函数控制:fun equals(otherObject:Any?): Booleanfun hashCode(): Int。如果您的类需要相等检查,您必须实现这两个。我们需要两个函数来进行相等性检查,这似乎有点奇怪。为什么只有equals()用于相等性检查是不够的?原因在于性能,精确的思路后面再讲。

首先,我们声明,如果我们为一些a1a2编写a1 == a2作为类A的实例,函数equals()在类A上被调用,并且只有当它返回true时,比较的结果也是true。对于==等式检查,那么equals()函数实际上就足够了。

对于地图,情况就不同了。例如,如果我们有一个映射,将某个类A的实例映射到任何对象

class A(val v:Int) {

    override fun hashCode():Int {
        return ...
    }
    override fun equals(other:Any?):Boolean {
        return ...
    }
}

val m = mapOf(A(7) to 8, A(8) to 9)

然后执行查找,如

val searchKey:A = ...
m[searchKey]

实际情况是这样的:

  • 通过对其调用hashCode()来计算searchKey的散列码。

  • []操作符(或get()函数)应用一种非常快速的算法,根据整数散列键找到一个条目。

  • 对于在散列关键字查找期间找到的条目,对所有可能的条目调用equals()。如果equals()找到了精确的条目,[]操作符返回该条目的相应值。

  • 如果哈希键查找失败或者所有后续的equals()检查失败,那么[]操作符也会失败并返回null.

我们观察到两件事:

  1. 只有当哈希代码查找成功时,equals()才会被调用。

  2. 为了使这个过程有意义,对于hashCode()函数,以下条件必须为真:(1)如果a == b,我们也需要a.hashCode() == b.hashCode()。②如果说a != b,在大多数情况下我们也应该有a.hashCode() != b.hashCode()。如果(1)不为真,地图查找功能将失败,如果(2)不为真,我们将不得不经常调用equals()

作为一个例子,考虑类

class Person(val lastName:String,
    val firstName:String,
    val birthday:String,
    val gender:Char)

我们基于所有属性实现了一个equals()函数:

class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char) {
    override fun equals(other:Any?):Boolean {
        if(other == null) return false
        if(other !is Person) return false
        if(lastName != other.lastName) return false
        if(firstName != other.firstName) return false
        if(birthday != other.birthday) return false
        if(gender != other.gender) return false
        return true
    }
}

如果提供比较的对象othernull或者不是Person的实例,fun equals()中的前两行返回null。你会在几乎所有的equals()实现中发现类似的代码行,尽管说你会在任何地方发现它们是夸张的;出于某种奇怪的原因,我们可能会接受与null或其他类型的比较。

因为如果other不是类型Person我们就已经完成了,从第三行开始,Kotlin 知道otherPerson的一个实例。这种自动类型检测有时被称为智能转换。接下来是对所有属性的逐步比较,只有当它们都匹配时,我们才返回true

对于一个hashCode()函数,你可能会想到很多算法,在网上你也会找到一些关于它的想法。幸运的是,我们不必在这方面花费太多的脑力;包java.util中的对象Objects为此提供了一个方便的函数,我们可以写:

class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char) {
    override fun equals(other:Any?):Boolean {
        if(other == null) return false
        if(other !is Person) return false
        if(lastName != other.lastName) return false
        if(firstName != other.firstName) return false
        if(birthday != other.birthday) return false
        if(gender != other.gender) return false
        return true
    }

    override fun hashCode(): Int {
        return Objects.hash(super.hashCode(),
            lastName, firstName, birthday, gender)
    }
}

对于这种明显的情况,即等式依赖于检查是否相等的所有属性,Kotlin 有一个捷径。我们已经讲过:数据类。它们完全基于所有属性实现了一个equals()和一个hashCode()函数。对于Person类,我们可以删除显式的equals()hashCode()函数,只需编写

data class Person(val lastName:String,
      val firstName:String,
      val birthday:String,
      val gender:Char)

练习 1

如果两个变量ab相同,下列哪一项是正确的?

  1. ab指的是同一个物体。

  2. a == b必然产生true

  3. a !== b必然产生false

练习 2

如果两个变量ab相等,a == b,下列哪一项是正确的?

  1. a.equals(b)一定是真的。

  2. a != b必然产生false

  3. a.hashCode() == b.hashCode()一定是真的。

十二、回到数学:函数式编程

如果你看一下本书到目前为止给出的例子和练习,你会发现我们在两种编程风格之间波动:

[statement1] // do something
[statement2] // do something
[statement3] // do something
...

object.
    doSomething1().
    doSomething2().
    doSomething3().
    ...

第一种风格是关于命令性地告诉程序必须做什么的序列,而第二种风格是关于在函数调用链中顺序地对对象应用函数。正因为如此,第一种风格也被称为命令式编程,第二种被称为函数式编程。函数式编程经常意味着使用函数作为其他函数的参数,这些函数被称为高阶函数。此外,函数式编程倾向于处理不可变的对象。

当使用命令式编程风格时,下面的观察变得很清楚:

  • 我们有一系列语句,包括if/elsewhen结构和循环。显然,语句的顺序很重要。

  • 每个语句执行一个可识别的程序活动,命令式编程乍一看会产生易于理解的程序。

  • 各种语句可以处理各种不同的对象。

  • 每个语句可能会也可能不会改变它所嵌入的对象的状态,以及更多相关对象的状态。显然,对于各种结构构造,如循环和条件分支,对于所有涉及的对象,状态和状态转换的复杂性没有真正的限制。

  • 语句包括一些函数调用,这些函数调用做了一些与它们的主要职责无关的意外事情。这种附属活动经常被称为副作用。因此,这些副作用可能是可预见的,也可能是不可预见的,可能会导致错误的程序活动。

相比之下,函数式编程具有以下特点:

  • 功能构造主要指单个对象或单个对象集合。但是,根据函数参数,其他对象或集合可能会进入函数调用链。

  • 函数式编程包括将函数作为函数参数来处理。因此,与命令式编程相比,它允许更高的抽象。

  • 通过将函数调用结果作为参数或输入传递给其他函数,函数程序部分允许无状态编程风格,避免了复杂的状态转换。

由于函数不引用也不改变对象状态,我们也回到了一个更像数学的函数概念。请记住,在数学中,函数有一个输入并从中产生一个输出,忽略任何可能影响计算的“状态”。面向对象使用稍微改变的函数概念,其中对象的状态对函数调用的结果起着重要作用。因此,函数式编程将函数的概念转移到更像数学的语义上。图 12-1 显示了命令式和函数式编程的比较。

img/476388_1_En_12_Fig1_HTML.jpg

图 12-1

函数式编程与命令式编程

在这一点上,我们并不偏爱这两种编程模式中的任何一种,并且通过观察每种编程风格的特征,您可以看到两者都有各自的优点和缺点。我们继续这种态度,但是指出根据情况,函数构造可以导致更优雅和更稳定的程序。Kotlin 允许这两种风格,对于每个任务,由您决定哪种范式最适合您的需求。

在本章的其余部分,我们将加深对函数构造的了解,这样你就有了一个改进的工具集来编写好的软件。

Kotlin 和函数式编程

虽然 Kotlin 是一种成熟的命令式语言,但它也允许使用函数式编程风格,因为它具有以下特性:

  • Kotlin has a function type declaration:

    ([parameters]) -> [result-type]
    
    

    where [parameters] is a comma-separated list of function parameter types. For example,

    val f : (Int,String) -> String = ...
    
    

    不能省略-> [result-type],所以如果一个函数没有返回任何东西,你就写-> Unit

  • 函数是一等公民:任何变量都可以有内置类型,可以是任何类的实例,也可以是函数。通过允许函数作为参数,函数可以是高阶函数。

    val f1 = { -> Log.d("LOG", "Hello Kotlin") }
    val f2 = { i:Int, s:String -> "${i}: ${s}" }
    ...
    fun ff(fun1: (Int,String) -> String):String {
        return fun1(7, "Hello")
    }
    ff(f2)
    ff( { i:Int,s:String -> "${i}- ${s}" } )
    
    
  • Kotlin has anonymous lambda functions; these are function literals that can be used as function invocation parameters. For example:

    val f = { i:Int, s:String ->
              i.toString() + ": " + s }
    fun fun1(p: (Int,String) -> String) {
        p(42, "Hello")
    }
    fun1 { i:Int, s:String -> i.toString() + ": " + s }
    
    

    在这里,Kotlin 推断出f必须是类型(Int, String) -> String

  • Kotlin 的标准库有很多针对对象、数组和集合的高阶函数。

  • 函数调用function({ [lambda-function] })可以缩写为

    function { [lambda-function] }
    
    
  • 函数调用function(par1, par2, ..., { [lambda-function] })可以缩写为function(par1, par2, ...) { [lambda-function] }

  • 在函数类型([parameters]) -> [result-type]中,参数通常是ParType的形式。一个特殊的接收器类型符号显示为A.(ParType)。在这种情况下,类型A是接收器类型,在类A的实例中调用的函数意味着在函数规范this中引用该实例。本章有一个专门的章节来讨论这种接收器类型符号。

  • Kotlin 变量可以是不可变的:val s = ...。不可变变量有助于避免状态处理并减少意外的副作用。

  • 来自单例对象的函数可以通过在前面加上两个冒号(::))来作为对象本身进行寻址。例如,如果你想使用来自object X { fun add(a:Int,b:Int):Int = a+b }的函数add(),你可以写

    object X {
      fun add(a:Int, b:Int): Int = a + b
    }
    ...
    val f : (Int,Int) -> Int = X::add
    
    
  • 来自类的函数可以通过在前面加上两个冒号(::)作为接收者类型的对象来寻址。比如:

    class X {
      fun add(a:Int, b:Int): Int = a + b
    }
    ...
    val f : X.(Int,Int) -> Int = X::add
    
    
  • 来自实例的函数可以通过在前面加上两个冒号(::))作为对象来寻址。比如:

    class X {
      fun add(a:Int, b:Int): Int = a + b
    }
    ...
    val x1 = X()
    val f : (Int,Int) -> Int = x1::add
    
    

没有名字的函数:λ函数

我们知道正常的函数看起来像

fun functionName([parameters]): ReturnType {
  ...
}

或者

fun functionName([parameters]): ReturnType = ...

如果函数可以简化为一个表达式。以这种方式声明的函数由functionName标识。问题是:不识别函数名,怎么可能有函数呢?对于答案,我们看包含数据的变量;我们在这里写道

val varName = 37

=右侧的值也没有标识名。我们只需要变量名来处理数据。如果我们看看赋给变量的函数,

val f = { i:Int, s:String -> i.toString() + ": " + s }

我们可以看到,{ ... }构造也没有标识名;函数被分配给变量使用。这样的函数是匿名的,通常被称为 lambda 函数。

注意

带有这种匿名函数的表达式有时也被称为λ演算

这同样适用于作为参数传递给其他函数的函数:

ff( { i:Int,s:String -> "${i}- ${s}" } )

这里我们又有一个没有名字的函数,或者 lambda 函数。

要调用 lambda 函数,您需要编写以下代码之一:

[lambda_function].invoke([parameters])
lambda_function

Lambda 函数可以有结果。与通过某个return语句返回值的普通函数相反,lambda 函数的结果是最后一行计算的结果。前面的例子

val f = { i:Int, s:String -> i.toString() + ": " + s }

因此,借助 lambda 函数的最后一行,返回参数Int的字符串表示,加上参数:,加上参数String

在只有一个参数的 lambda 函数中,为了简洁起见,可以省略参数声明,而可以使用特殊标识符it来引用参数。因此,以下两个语句是等效的:

{ par ->
    ... // do something with 'par' }
{
    ... // do something with 'it' }

练习 1

编写一个 lambda 函数:一个接受一个s:String和一个num:Int并输出一个包含snum副本的字符串的函数。

练习 2

重写

val f : (String) -> String = { s:String -> s + "!" }

要用it来代替。

如果 lambda 函数有一个或多个定义中不需要的参数,可以使用下划线通配符(_)作为参数名:

val f : (String, Int) -> String = { s:String, _ ->
    // the Int parameter is not used
    s + "!"
}

再次循环

在第九章中我们了解到我们可以通过写data.forEach(...)data.forEachIndexed(...)来迭代数组或集合(集合,列表)的元素:

val arr = arrayOf("Joe", "Isabel", "John" }
arr.forEach { name ->
    Log.d("A name: ${name}")
}
arr.forEachIndexed { i,s ->
    Log.d("Name #${i}: ${name}")
}

这里的Log来自包android.util,所以您必须导入它:

import android.util.Log

虽然乍一看,forEachforEachIndexed后面的{ }看起来像是一个语句块,但是我们可以通过查看->看到,实际上forEachforEachIndexed实际上都是带有一个 lambda 函数作为参数的函数。那么,也有可能写出arr.forEach({ ... })arr.forEachIndexed({ ... });括号可以省略,就像 Kotlin 中的情况一样,如果它们只是用花括号括起来的话。

在 Android Studio 中,我们还可以查看任何函数调用的源代码。为此,例如,将光标放在forEach上,然后按 Ctrl+b。Android Studio 随后会打开该函数的源代码并显示如下:

public inline fun <T> Array<out T>.forEach(
        action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

这里我们再次看到,在forEach之后的是一个作为函数参数的函数。

注意

按 Ctrl+B 是了解 Kotlin 幕后情况的好方法。广泛使用它来理解 Kotlin 结构和功能。

因为forEachforEachIndexed是函数而不是语言结构,它们可以直观地应用于任何看起来包含可以迭代的东西的对象。这包括数组和集合,它们是对数组和集合应用其他函数的结果。因此,我们可以在以循环结束的函数链中包含过滤器和映射,如

originalCollection.
      filter([filter_function]).
      map([mapping_function]).
      take(37).
      forEach { elem ->
          ...
      }

在开始循环之前,我们首先应用一个过滤器,然后一个映射,然后减少到第一个37元素。我们可以看到,由于函数被允许作为函数参数,我们可以实现一个函数链,并避免中间变量作为数据持有者。

接收器的功能

被认为是函数对象并嵌入在上下文中的函数,例如,类中的函数,被称为具有接收器类型的函数。您将它们声明如下:

val f : ReceiverType.([parameters]) = ...

这样一个函数就像是类ReceiverType的成员函数,在函数实现内部,你可以使用指向实例的this,。例如,在

class A {
    var d:Double = 0.0
    fun plus(x:Double) = d + x
}
val f : A.(Double) -> Double =
      { x:Double -> this.d - x }
fun A.minus(x:Double) = f

函数f就是这样一个带有接收器类型的函数。我们用它来用一个minus()函数扩展类A,并且f实现内部的this.d指向接收者类型内部的属性d,在本例中为A

在上一节中,我们已经注意到,对类内部函数的直接引用自动就是这样一个具有接收器类型的函数,因为它只在类的环境中工作:

class X {
  fun add(a:Int, b:Int): Int = a + b
}
...
val f : X.(Int,Int) -> Int = X::add

内嵌函数

请看这段代码:

class A {
  fun function1(i:Int, f:(Int) -> String): String {
      return f(i)
  }
  fun function2() {
      val a = 7

      val s = function1(8) {
          i -> "8 + a = " + (i+a) }
  }
}

在对function1()的调用中,我们以 lambda 函数i -> ...的形式传递一个函数对象。这个函数对象必须在运行时创建,此外,编译器必须允许将本地属性a传递给该对象。这带来了显著的性能损失。更准确地说,Kotlin 编译器会产生类似这样的结果:

public class A {
    public String function1(int i,
          Function1<? super Integer, String> f) {
        return f.invoke(i);
    }

    public void function2() {
        int a = 7;
        String s2 = this.function1(8,
              new Function1<Integer, String>(a){
            final int $a;
            public String invoke(int i) {
                return "8 + a = " + (i + this.$a);
            }
            {
                this.$a = n;
                super(1);
            }
        });
    }
}

这是 Java 语言代码,但是不用深入细节,我们看到通过new Function1(...)一个函数对象必须被实例化,并且在它里面属性a的副本将被创建。

如果这种性能损失造成了问题,那么function1()可以被内联:

class A {
  inline fun function1(i:Int, f:(Int) -> String): String
  {
      return f(i)
  }
  fun function2() {
      val a = 7
      val s = function1(8) {
          i -> "8 + a = " + (i+a) }
  }
}

这是什么意思?它基本上是说,每当使用内联函数时,不会发生实际的函数调用,而是将函数代码复制到使用该函数的地方。再次查看编译器输出,这次我们得到

public class A {
  public String function1(int i,
        Function1<? super Integer, String> f) {
      return f.invoke(i);
  }

  public void function2() {
      int a = 7;
      int i$iv;
      int i = i$iv = 8;
      String s2 = "8 + a = " + (i + a);
  }
}

您可以从内部function2()看到,内联函数function1没有被调用;相反,这个片段

int i$iv;
int i = i$iv = 8;
String s2 = "8 + a = " + (i + a);

替换函数调用。没有发生对象实例化,因此与没有内联的变体相比,这段代码运行得更快。

使用内联函数会产生一些不寻常的特性。例如,return语句的行为与内联函数不同。此外,可以只内嵌专用的 lambda 函数参数,而让其他函数创建函数对象。此外,内联函数支持一种特殊的类型参数,称为具体化类型参数 ,允许在运行时访问该类型的参数。这里不赘述;如果你感兴趣,请参考在线 Kotlin 文档中的函数。

过滤

如果你有一些对象的列表,比如一个data class Employee(val firstName:String, val lastName:String, val ssn:String, val yearHired:Int)的实例,在算法中你经常需要根据一些标准提取列表成员。使用命令式编程风格,这通常会产生如下代码片段:

data class Employee(val firstName:String,
      val lastName:String,
      val ssn:String,
      val yearHired:Int)
val employees = listOf(
    Employee("John", "Smith", "123-12-0001", 1987),
    Employee("Linda", "Thundergaard", "123-12-0002", 1987),
    Employee("Lewis", "Black", "123-12-0003", 1977),
    Employee("Evans", "Brightsmith", "123-12-0004", 1991)
)
val before1990 = mutableListOf<Employee>()
for(empl in employees) {
    if(empl.yearHired < 1990) before1990.add(empl)
}
... // do something with before1990

这段代码看起来非常容易理解,并且似乎充分解决了过滤任务,但是如果我们更仔细地观察它,就会发现有几个问题。

  • 在开始循环之前,我们需要在单独的语句中创建接收列表。结果列表创建与循环分离;代码并不阻止我们在列表创建和循环之间添加更多的语句,例如,因为未来的需求。这种分离可能会引入复杂的状态转换,从而使程序不稳定。

  • 在循环内部,结果列表只是一个局部变量;循环存在的唯一目的是填充结果列表,但是代码并不阻止我们在那里做其他事情,最终降低代码的可读性。

  • 如果列表变得非常长,我们可能会尝试在循环内部并行化代码;也就是说,让几个进程同时进行过滤。这很容易导致并发问题,因为before1990变量只是一个普通的局部属性。让几个进程同时访问同一个集合经常会导致数据一致性失败。

  • 有了更复杂的过滤标准,我们可能会在循环中各种if-else/when分支的复杂堆叠中结束。

对几乎所有这些问题的补救措施包括切换到功能代码:

data class Employee(val firstName:String,
      val lastName:String,
      val ssn:String,
      val yearHired:Int)
val employees = listOf(
    Employee("John", "Smith", "123-12-0001", 1987),
    Employee("Linda", "Thundergaard", "123-12-0002", 1987),
    Employee("Lewis", "Black", "123-12-0003", 1977),
    Employee("Evans", "Brightsmith", "123-12-0004", 1991)
)
val before1990 = employees.filter {
    it.yearHired < 1990 }.toList()
... // do something with before1990

这里我们可以避免在filter()的参数中写emp -> ...,因为只有一个函数参数,并且我们使用自动的it变量。在filter()之后,我们可以插入更多的过滤器,或者一个映射函数,就像我们在第九章中看到的那样。

练习 3

通过应用一个只允许名字以 l 开头的雇员通过的过滤器,创建另一个列表startsWithL。注意:String有一个startsWith()函数可以用于这个目的。

十三、关于类型安全:泛型

泛型是一个术语,用来表示一组允许我们向类型添加类型参数的语言特性。例如,考虑一个简单的类,它具有以Int对象的形式添加元素的功能:

class AdderInt {
    fun add(i:Int) {
        ...
    }
}

另一个用于String对象:

class AdderString {
    fun add(s:String) {
        ...
    }
}

除了在add()函数内部发生的事情之外,这些类看起来非常相似,所以我们可以考虑一个语言特性来抽象要添加的元素的类型。这样的语言特性存在于 Kotlin 中,它被称为泛型。相应的结构如下:

class Adder<T> {
    fun add(toAdd:T) {
        ...
    }
}

其中T类型参数。在这里,除了T,任何其他名称都可以用于类型参数,但是在许多项目中,您经常会发现TRSUAB作为类型参数名称。

为了实例化这样的类,编译器必须知道该类型。要么必须显式指定类型,如

class Adder<T> {
    fun add(toAdd:T) {
        ...
    }
}
val intAdder = Adder<Int>()
val stringAdder = Adder<String>()

或者编译器必须能够推断类型,如

class Adder<T> {
    fun add(toAdd:T) {
        ...
    }
}
val intAdder:Adder<Int> = Adder()
val stringAdder:Adder<String> = Adder()

注意

泛型是编译时构造。在编译器生成的代码中,不会出现泛型信息。这种效应通常被称为型擦除。

我们已经在书中多次使用了这种通用类型。您可能还记得,作为两个数据元素的持有者,我们讨论过参数化的Pair类型:

val p1 = Pair<String, String>("A", "B")
val p2 = Pair<Int,String>(1, "A")

当然,我们也谈到了各种集合类型,例如:

val l1: List<String> = listOf("A","B","C")
val l2: MutableList<Int> = mutableListOf(1, 2, 3)

到目前为止,我们只是照原样接受了泛型,没有进一步解释它们。毕竟写List<String>,我们说的一串的推演是显而易见的。

一旦我们开始更彻底地审视收藏品,这个故事就变得有趣了。问题是:如果我们有一个MutableList<Any>和一个MutableList<String>,它们是如何关联的?我们可以写val l:MutableList<Any> = mutableListOf<String>("A", "B")吗?或者换句话说,MutableList<-String>MutableList<Any>的子类吗?事实并非如此,在本章的剩余部分,我们将深入讨论泛型,并试图理解类型关系。

简单泛型

首先,让我们解决基本问题。要对类或接口进行类型参数化,可以在类型名后的尖括号内添加一个逗号分隔的形式类型参数列表:

class TheClass<[type-list]> {
    [class-body]
}
interface TheInterface<[type-list]> {
    [interface-body]
}

在类或接口内部,包括任何构造函数和init{}块,你可以像其他类型一样使用类型参数。例如:

class TheClass<A, B>(val p1: A, val p2: B?) {
    constructor(p1:A) : this(p1, null)
    init {
        var x:A = p1
        ...
    }
    fun function(p: A) : B? = p2
}

练习 1

类似于Pair类,创建一个可以保存四个数据元素的类Quadruple。使用示例IntIntDoubleString类型元素创建一个实例。

声明方差异

如果我们谈论泛型,术语方差表示在赋值中使用更具体或更不具体类型的能力。知道了AnyString更不具体,方差就出现在以下问题中:是否可能出现以下情况之一:

class A<T> { ... }
var a = A<String>()
var b = A<Any>()

a = b // variance?
... or ...
b = a // variance?

为什么这对我们很重要?如果我们看看类型安全,这个问题的答案就变得很清楚了。考虑下面的代码片段:

class A<T> {
    fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()

b = a // variance?
b.add(37)

37添加到A<Any>不会造成问题,因为任何类型都是Any的子类。然而,因为b通过b = a指向了A<String>的一个实例,我们会得到一个运行时错误,因为37不是一个字符串。Kotlin 编译器认识到了这个问题,不允许使用b = a赋值。

同样,分配a = b也会带来一个问题。这一点更加明显,因为a只适用于String元素,不能像b那样处理Int类型的值。

class A<T> {
    fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()

a = b // variance?
val extracted:String = a.extract()

最后一条语句中的a.extract()可以同时计算为AnyString类型,因为b和现在的a可以包含Int对象,但是a不允许包含Int对象,因为它只能处理String元素。因此 Kotlin 也不允许a = b

我们能做什么?不允许任何差异可能是一种选择,但这太苛刻了。同样,查看分配了b = a的第一个样本,我们可以看到写入b导致了错误。读书怎么样?考虑一下这个:

class A<T> {
    fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()

b = a // variance?
val extracted:String = b.extract()

就类型而言,最后一个操作是安全的,所以我们实际上在这里应该不会有问题。

完全相反的情况,取a = b样本并应用写操作而不是读操作,如

class A<T> {
    fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()

a = b // variance?
a.add("World")

应该也不成问题。我们可以给ab添加字符串。

为了使这种差异成为可能,Kotlin 允许我们向通用参数添加一个差异注释。如果我们将out注释添加到类型参数中,第一个带有b = a的示例会编译:

class A<out T> {
    fun extract(): T = ...
}
var a = A<String>()
var b = A<Any>()

b = a // variance? YES!
val extracted:String = b.extract()
// OK, because we are reading!

如果我们将in注释添加到类型参数中,第二个带有a = b的示例将会编译:

class A<in T> {
    fun add(p:T) { ... }
}
var a = A<String>()
var b = A<Any>()

a = b // variance? YES!.add("World")
// OK, because we are writing!

因此,通过将inout variance 注释添加到类型参数中,并限制类操作只允许泛型类型的输入或泛型类型的输出,在 Kotlin 中就有可能出现差异!如果两者都需要,可以使用不同的构造,如本章后面的“类型投影”一节所述。

注意

类的out方差也被称为协方差,而in方差被称为方差

名称声明方差异源于在类的声明中声明inout差异。其他语言,比如 Java,使用一种不同类型的方差,这种方差在使用类时生效,因此被称为使用方方差。

不可变集合的差异

因为不可变集合不能被写入,Kotlin 自动使它们协变。如果您愿意,可以考虑将 Kotlin 的out variance 注释隐式添加到不可变集合中。

由于这个事实,一个List<SomeClass>可以被分配给一个List<SomeClassSuper>,其中SomeClassSuperSomeClass的超类。例如:

val coll1 = listOf("A", "B") // immutable
val coll2:List<Any> = coll1  // allowed!

类型投影

在上一节中,我们看到对于out样式变化,相应的类不允许使用泛型类型作为函数参数,对于in样式变化,我们相应地不能使用返回泛型类型的函数。当然,如果我们在一个类中需要两种功能,这是不令人满意的。Kotlin 也有这类需求的答案。它被称为型投影,因为它的目标是在使用一个类的不同函数时的方差,所以它是使用方方差的 Kotlin 等价物。

想法如下:我们仍然使用inout方差注释,但是我们没有为整个类声明它们,而是将它们添加到函数参数中。我们稍微改写了上一节的示例,并添加了inout方差注释:

class Producer<T> {
    fun getData(): Iterable<T>? = null
}
class Consumer<T> {
    fun setData(p:Iterable<T>) { }
}

class A<T> {
    fun add(p:Producer<out T>) { }
    fun extractTo(p:Consumer<in T>) { }
}

add()函数中的out表示我们需要一个产生T对象的对象,extractTo()函数中的in表示一个消耗T对象的对象。让我们看一些客户端代码:

var a = A<String>()
var b = A<Any>()

var inputStrings = Producer<String>()
var inputAny = Producer<Any>()
a.add(inputStrings)
a.add(inputAny)            // FAILS!
b.add(inputStrings)        // only because o "out"
b.add(inputAny)

var outputAny = Consumer<Any>()
var outputStrings = Consumer<String>()
a.extractTo(outputAny)     // only because of "in" a.extractTo(outputStrings)
b.extractTo(outputAny)
b.extractTo(outputStrings) // FAILS!

你可以看到a.add(inputAny)失败了,因为inputAny产生了各种各样的对象,而a只能接受String对象。类似地,b.extractTo(outputStrings)失败,因为b包含任何类型的对象,而outputStrings只能接收String对象。到目前为止,这与方差无关。这个故事对b.add(inputStrings)来说变得有趣了。允许将字符串添加到A<Any>的行为当然是有意义的,但是它只在我们将out投影添加到函数参数时才起作用。类似地,a.extractTo(outputAny)虽然肯定是可取的,但只是因为in投影才起作用。

恒星投影

如果您有一个带有inout方差注释的类或接口,您可以使用特殊的通配符*,其含义如下:

  • 对于out差异标注,*表示out Any?

  • 对于in差异标注,*表示in Nothing

记住Any是任何类的超类,Nothing是任何类的子类。

例如:

interface Interf<in A, out B> {
    ...
}

val x:Interf<*, Int> = ...
    // ... same as Interf<in Nothing, Int>

val y:Interf<Int, *> = ...
    // ... same as Interf<Int, out Any?>

如果您对类型一无所知,但仍然希望满足类或接口声明规定的差异语义,则可以使用星号通配符。

通用函数

Kotlin 中的函数也可以是泛型的,这意味着它们的参数或它们的一些参数可以具有泛型类型。在这种情况下,通用类型指示符必须作为逗号分隔的列表添加到function关键字之后的尖括号中。泛型类型也可以出现在函数的返回类型中。这里有一个例子。

fun <A> fun1(par1:A, par2:Int) {
    ...
}

fun <A, B> fun2(par1:A, par2:B) {
    ...
}

fun <A> fun3(par1:String) : A {
    ...
}

fun <A> fun4(par1:String) : List<A> {
    ...
}

要调用这样的函数,原则上必须在尖括号中的函数名称后指定具体类型:

fun1<String>("Hello", 37)

fun2<Int, String>(37, "World")

val s:String = fun3<String>("A")

然而,正如 Kotlin 中经常出现的情况,如果 Kotlin 可以推断类型,则可以省略类型参数。

通用约束

到目前为止,对于泛型类型标识符在实例化期间可以映射到的类型没有任何限制。因此,在class TheClass<T>中,T通用类型可以是任何东西,TheClass<Int>TheClass<String>TheClass<Any>或其他任何东西。但是,可以将类型限制为某个类或接口或其子类型之一。为了这个目标,你写道

<T : SpecificType>

印度历的 7 月

class <T : Number> { ... }

它将T限制在一个Number或它的任何子类中,比如IntDouble

这很有用。例如,考虑一个允许我们向Double属性添加内容的类。

class Adder<T> {
    var v:Double = 0.0
    fun add(value:T) {
        v += value.toDouble()
    }
}

你明白为什么这个代码是非法的吗?我们说value的类型是T,但是类不知道在实例化过程中T是什么,所以不清楚T.toDouble()函数是否实际存在。因为我们知道在编译之后所有的类型都被删除了,编译器没有机会检查是否有一个toDouble(),因此它将代码标记为非法。如果你查看 API 文档,你会发现IntLongShortByteFloatDouble都是kotlin.Number的子类,它们都有一个toDouble()函数。如果我们有办法说TNumber或者它的子类,我们就可以使代码合法。

Kotlin 确实有一种方法来限制泛型类型,它读起来是<T : SpecificType>。因为T然后被限制在SpecificType或者它在类型层次结构中更低的任何子类型,这也被称为upper type bound。为了使我们的Adder类合法,我们所要做的就是写

class Adder<T : Number> {
    var v:Double = 0.0
    fun add(value:T) {
        // T is a Number, so it _has_ a toDouble()
        v += value.toDouble()
    }
}

这种类型约束也可以添加到泛型函数中,所以我们实际上可以将Adder类重写为:

class Adder {
    var v:Double = 0.0
    fun <T:Number> add(value:T) {
        v += value.toDouble()
    }
}

这具有特别的优点,即在实例化期间不需要解析泛型类型。

val adder = Adder()
adder.add(37)
adder.add(3.14)
adder.add(1.0f)

请注意,与类继承不同,类型界限可以多次声明。这在尖括号内是不可能发生的,但是有一个特殊的构造来处理这种情况。

class TheClass<T> where T : UpperBound1,
                   T : UpperBound2, ...
{
    ...
}

或者

fun <T> functionName(...) where T : UpperBound1,
                   T : UpperBound2, ...
{
    ...
}

对于一般函数。

你可能不得不习惯的一点是,泛型类可能出现在冒号(:)的两边,这是完全可以接受的

class TheClass <T : Comparable<T>> {
    ...
}

来表示 T 必须是Comparable的子类。

练习 2

用类型参数T和合适的类型绑定编写一个泛型类Sorter,它有一个属性val list:MutableList<T>和一个函数fun add(value:T)。每次调用函数时,必须将参数添加到列表中,并且必须根据列表属性的自然排序顺序对其进行排序。

十四、添加提示:注解

注解用于向代码中添加元信息。那是什么意思?考虑以下类:

class Adder {
    fun add(a:Double, b:Double) = a + b
}
class Subtractor {
    fun subtract(a:Double, b:Double) = a - b
}

如果我们有一个更大的算术计算项目,其中各种操作由像这里的AdderSubtractor这样的类来处理,我们可以有这样的东西

val eng = CalculationEngine()
...
eng.registerAdder(Adder::class, "add") eng.registerSubtractor(Subtractor::class, "subtract")
...

用于注册特定的低级操作。

然而,我们可以遵循一种不同的方法,操作者以某种方式向框架宣布他们的能力。他们可以通过特殊的文档标记来做到这一点,例如

/**
 * @Operator: ADDING
 * @Function: add
 */
class Adder {
    fun add(a:Double, b:Double) = a + b
}

/**
 * @Operator: SUBTRACTING
 * @Function: subtract
 */
class Subtractor {
    fun subtract(a:Double, b:Double) = a - b
}

然后,一些解析器可以查看源代码,找出各种操作符需要哪些类和函数。

注意

一个框架是为软件提供脚手架结构的类、接口和单例对象的集合。框架本身并不是一个可执行的程序,而是一个软件项目使用框架来建立一个标准化的结构。因此,使用特定框架的不同项目表现出相似的结构,如果开发人员知道一个嵌入到特定框架中的项目,那么理解使用相同框架的其他项目将会更容易。

这种让类向程序声明自己的方法经常在服务器环境中使用,在这种环境中,程序需要能够通过网络与客户机通信。

然而,这种方法有一个问题。因为元信息是从文档内部呈现的,所以编译器不可能检查标签的正确性。关于编译器,文档的内容是完全不重要的,并且应该是不重要的,因为这是语言规范所说的。

Kotlin 中的注解

这就是注解进入游戏的地方。它们正是为了这种任务而存在的:不干涉类的主要职责,而是为程序或框架提供元信息,用于维护或注册目的。注解如下所示:

@AnnotationName

或者

@AnnotationName(...)

如果有参数。许多语言元素都可以用这样的注解来标记:文件、类、接口、单例对象、函数、属性、lambdas、语句,甚至其他注解。前面的计算引擎示例的运算符类可以读作

@Operator(ADDING)
class Adder {
    @OperatorFunction
    fun add(a:Double, b:Double) = a + b
}

@Operator(SUBTRACTING)
class Subtractor {
    @OperatorFunction
    fun subtract(a:Double, b:Double) = a - b
}

现在编译器的情况更好了。因为注解是语言的一部分,编译器可以检查它们是否存在,拼写是否正确,是否提供了正确的参数。

在接下来的部分中,我们首先讨论注解特征,然后讨论 Kotlin 提供的注解。然后,我们将介绍如何构建和使用我们自己的注解。

注解特征

注解由注解类声明,如下所示:

annotation class AnnotationName

我们将在后面的章节中介绍如何构建我们自己的注解。现在我们提到声明,因为注解有它们自己的注解描述的特征,这些注解是元注解:

@Target(...)
@Retention(...)
@Repeatable
@MustBeDocumented
annotation class AnnotationName

您可以按任意顺序使用它们的任意组合,如果没有指定,它们都有默认值。我们在这里描述它们,包括可能的参数。

  • @Target(...)

    Here you specify the possible element types to which the annotation can be applied. The parameter is a comma-separated list of any of the following (all of them are fields of the enumeration kotlin.annotation.AnnotationTarget):

    • CLASS:所有的类、接口、单例对象和注解类。

    • ANNOTATION_CLASS:仅注解类。

    • PROPERTY:属性。

    • FIELD:作为属性的数据持有者的字段。请注意,通过 getters 和 setters 获得的属性不一定需要字段。然而,如果有一个字段,这个注解目标指向那个字段。您将它与PROPERTY目标一起放在属性声明的前面。

    • LOCAL_VARIABLE:任意局部变量(函数内的valvar)。

    • VALUE_PARAMETER:函数或构造函数参数。

    • CONSTRUCTOR:一级或二级建造师。如果您想要注解一个主构造函数,您必须使用添加了constructor关键字的符号;例如,class Xyz @MyAnnot constructor(val p1:Int, ...).

    • FUNCTION:函数(不包括构造函数)。

    • PROPERTY_GETTER:属性 getters。

    • PROPERTY_SETTER:财产设定者。

    • TYPE:类型注解,如val x: @MyAnnot Int = ...

    • EXPRESSION:语句(必须包含一个表达式)。

    • FILE:文件标注。您必须在package声明之前指定这一点,另外在@和注解名之间添加一个file:,如@file:AnnotationName

    • 我们还没有讨论类型别名。它们只是类型的新名称,如在typealias ABC = SomeClass<Int>中。这种注解类型适用于这样的typealias声明。

    如果未指定,目标为CLASSPROPERTYLOCAL_VARIABLEVALUE_PARAMETERCONSTRUCTORFUNCTIONPROPERTY_GETTERPROPERTY_SETTER

  • @Retention(...)

    This specifies where the annotation information goes during compilation and whether it is visible using one of the following (all are fields from the enumeration class kotlin.annotation.AnnotationRetention):

    • SOURCE:注解仅存在于源代码中;编译器会移除它。

    • BINARY:注解存在于编译后的类、接口或单例对象中。使用反射在运行时查询注解是不可能的。

    • RUNTIME:注解存在于编译后的类、接口或单例对象中,在运行时无法使用反射查询注解。

    默认值为运行时。

  • @Repeatable

    如果您想让注解不止出现一次,请添加此选项。

  • @MustBeDocumented

    如果您希望注解显示在公共 API 文档中,请添加此选项。

您可以看到,对于类、接口、单例对象、属性和本地属性,如果您希望注解在编译后的文件中明显显示,您不必指定特殊的特征。

应用注解

通常,注解写在要应用注解的元素的前面。这个故事变得有点复杂,因为元素的含义并不总是很清楚。考虑这个例子:

class Xyz {
    @MyAnnot var d1:Double = 1.0
}

这里我们有四个可以应用注解的元素:属性、属性 getter、属性 setter 和数据字段。出于这个原因,Kotlin 以在@和注解名之间写一个qualifier:的形式引入了使用站点目标。以下是可用的使用站点目标:

  • file

    We know that a Kotlin file can contain properties and functions outside classes, interfaces, and singleton objects. For an annotation applying to such a file, you write @file:AnnotationName in front of the package declaration. For example:

    @file:JvmName("Foo")
    package com.xyz.project
    ...
    
    

    将内部创建的类命名为Foo

  • property

    批注与属性相关联。注意,如果使用 Java 访问 Kotlin 类,这个注解对 Java 是不可见的。

  • field

    注解与属性后面的数据字段相关联。

  • get

    注解与属性 getter 相关联。

  • set

    注解与属性 setter 相关联。

  • receiver

    注解与扩展函数或属性的接收器参数相关联。

  • param

    注解与构造函数参数相关联。

  • setparam

    注解与属性 setter 参数相关联。

  • delegate

    注解与存储委托实例的字段相关联。

如果没有指定 use-site 目标,@Target元注解用于查找要注解的元素。如果有几种可能,排名是param > property > field

以下代码显示了各种注解应用示例(为简单起见,所有注解都没有参数,并假定指定了正确的@Target):

// An annotation applying to a file (the implicit
// internal class generated)
@file:Annot
package com.xyz.project
...

// An annotation applying to a class, a singleton
// object, or an interface
@Annot class TheName ...
@Annot object TheName ...
@Annot interface TheName ...

// An annotation applying to a function
@Annot fun theName() { ... }

// An annotation applying to a property
@property:Annot val theName = ...
@Annot var theName = ...
class SomeClass(@property:Annot var param:Type, ...) ...

// An annotation applying to a function parameter
f(@Annot p:Int, ...) { ... }

// An annotation applying to a constructor
class TheName @annot constructor(...) ...

// An annotation applying to a constructor parameter
class SomeClass(@param:Annot val param:Type, ...) ...

// An annotation applying to a lambda function
val f = @annot { par:Int -> ... }

// An annotation applying to the data field
// behind a property
@field:Annot val theName = ...
class SomeClass(@field:Annot val param:Type, ...) ...

// An annotation applying to a property setter
@set:Annot var theName = ...
var theName = 37 @Annot set(...) { ... }
class SomeClass(@set:Annot var param:Type, ...) ...

// An annotation applying to a property getter

@get:Annot var theName = ...
var theName = 37 @Annot get() = ...
class SomeClass(@get:Annot var param:Type, ...) ...

// An annotation applying to a property setter
// parameter
var theName:Int = 37
    set(@setparam:Annot p:String) { })

// An annotation applying to a receiver
@receiver:Annot fun String.xyz() { }

// An annotation applying to a delegate
class Derived(@delegate:Annot b: Base) : Base by b

要使用注解作为注解参数,您不需要添加一个@前缀:

@Annot(AnotherAnnot)

带数组参数的批注

使用数组作为注解构造函数参数很容易:只需在注解声明中使用vararg限定符,在注解实例化中使用逗号分隔的参数列表:

annotation class Annot(vararg val params:String)
...
@Annot("A", "B", "C", ...) val prop:Int = ...

如果您需要使用包含在项目中的 Java 库中的带有单个数组参数的注解,该参数会自动转换成一个vararg参数,所以您基本上可以这样做:

@field:JavaAnnot("A", "B", "C", ...) val prop:Int = ...

如果注解有几个命名参数,其中一个或几个是数组,则使用特殊的数组文字符号:

@Annot(param1 = 37, arrParam = [37, 42, 6], ...)

阅读注解

要读取保留类型为SOURCE的注解,您需要一个特殊的注解处理器。请记住,对于SOURCE类型的注解,Kotlin 编译器会在编译过程中移除注解,因此在这种情况下,我们必须在编译器开始工作之前安装一些软件来查看源代码。大多数源类型注解处理发生在较大的服务器框架项目中;在这里,注解被用来生成一些合成的 Kotlin 或 Java 代码,这些代码将类粘合在一起,以模拟复杂的数据库结构。有一个特殊的插件用于此目的,KAPT,它允许包含这样的源类型注解预处理程序。

您可以在在线 Kotlin 文档中找到更多关于 KAPT 用法的信息。在本节的剩余部分,我们将讨论RUNTIME保留类型注解处理。

为了读取由 Kotlin 编译器编译并最终由运行时引擎执行的字节码中的注解,使用了反射 API 。我们将在本书的后面讨论反射 API 这里我们只提到注解处理方面。

注意

要使用反射,kotlin-reflect.jar必须在类路径中。这意味着你必须在你的模块的build.gradle文件的依赖部分添加implementation "org.jetbrains.kotlin: kotlin-reflect:$kotlin_version"

要获取最基本元素的注解,请参见表 14-1 ,该表描述了如何获取注解或注解列表。

表 14-1。

按元素标注

|

元素

|

阅读注解

| | --- | --- | | 类、单例对象和接口 | 使用TheName::class.annotations要获得kotlin.Annotation对象的列表,您可以进一步研究。例如,您可以使用属性.annotationClass来获取每个注解的类。如果您有一个属性,并且首先需要获取相应的类,请使用property::class.annotations要阅读某个注解,请使用val annot = TheName::class.findAnnotation<AnnotationType>()在这里用注解的类名代替AnnotationType。例如,您可以从这里通过annot?.paramName读取注解的参数。 | | 性能 | 使用val prop = ClassName::propertyNameval annots = prop.annotationsval annot = prop.findAnnotation<AnnotationType>()通过名称获取一个属性,并从中获得一个注解列表或搜索某个注解。 | | 菲尔茨 | 要访问字段的注解,请使用val prop = ClassName::propertyNameval field = prop.javaFieldval annotations = field?.annotations | | 功能 | 要通过名称访问非重载功能,请写入TheClass::functionName。如果您有几个函数使用相同的名称,但参数不同,您可以编写val funName = "functionName"    // <- choose your ownval pars = listOf(Int::class)    // <- choose your ownval function =     TheClass::class.    declaredFunctions.filter {        it.name == funName }    ?.find { f ->      val types = f.valueParameters.map{          it.type.jvmErasure}      types == pars``}一旦有了这个函数,您就可以使用.annotations来查看注解列表,或者使用.findAnnotation<AnnotationType>()来搜索某个注解。 |

内置注解

Kotlin 从一开始就提供了一些注解。表 14-2 显示了一些通用注解。

img/476388_1_En_14_Fig1_HTML.jpg

图 14-1。

在 Android Studio 中隐藏注解

表 14-2。

内置注解:常规

|

注解名称

|

包裹

|

目标

|

描述

| | --- | --- | --- | --- | | Deprecated | 我的锅 | 类、批注类、函数、属性、构造函数、属性设置器、属性获取器、类型别名 | 接受三个参数:message:StringreplaceWith:ReplaceWith = ReplaceWith("")level:DeprecationLevel = DeprecationLevel.WARNING将元素标记为已弃用。DeprecationLevel为字段枚举:WARNINGERRORHIDDEN | | ReplaceWith | 我的锅 | — | 需要两个参数:expression:Stringvararg imports:String。使用它来指定@Deprecated中的替换代码片段。 | | Suppress | 我的锅 | 类、批注类、函数、属性、字段、局部变量、值参数、构造函数、属性设置器、属性获取器、类型、类型别名、表达式、文件 | 接受一个 vararg 参数:names:String。保留类型为SOURCE。使用它来禁止编译器警告。names参数是一个逗号分隔的警告消息标识符列表。不幸的是,找到编译器警告标识符的详尽列表并不容易,但 Android Studio 有所帮助:一旦出现编译器警告,相应的构造就会突出显示,当光标在其上时按 Alt+Enter 允许我们生成相应的 suppress 注解。参见图 14-1 (使用箭头键在菜单中导航)。 |

自定义注解

要定义自己的简单注解,您需要编写

@Target(...)
@Retention(...)
@Repeatable
@MustBeDocumented
annotation class AnnotationName

对于注解的注解(即元注解),注意它们都是可选的,顺序是自由的。有关它们的含义,请参阅本章前面的“注解特征”一节。

如果需要带参数的批注,可以在声明中添加一个主构造函数:

[possibly meta-annotations]
annotation class AnnotationName(val p1:Type1, val p2:Type2, ...)

其中允许以下参数类型:对应于原始类型的类型(即,ByteShortIntLongCharFloatDouble)、字符串、类、枚举、其他注解以及它们的数组。您可以添加vararg来获得可变数量的参数。注意,对于用作其他注解的参数的注解,参数注解的@被省略。

作为一个例子,我们以类Calculator的形式启动一个计算引擎。我们引入一个注解来避免被0.0除。注解如下:

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotZero()

对于类和两个操作符dividemultiply,我们写:

class Calculator {
  enum class Operator(val oper:String) {
      MULTIPLY("multiply"),
      DIVIDE("divide")
  }

  fun operator(oper:Operator,
               vararg params:Double): Double {
      val f = Calculator::class.declaredFunctions.
            find { it.name == oper.oper }
      f?.valueParameters?.forEachIndexed { ind, p ->

          p.findAnnotation<NotZero>()?.run {
              if (params[ind] == 0.0)
                  throw RuntimeException(
                  "Parameter ${ind} not unequal 0.0")
          }
      }
      val ps = arrayOf(this@Calculator,
            *(params).toList().toTypedArray<Any>())
      return (f?.call(*ps) as Double?) ?: 0.0
  }

  fun multiply(p1:Double, p2:Double) : Double {
      return p1 * p2
  }

  fun divide(p1:Double, @NotZero p2:Double) : Double {
      return p1 / p2
  }
}

operator()功能的作用如下:

  • 它查找对应于第一个参数的函数。Calculator::class.declaredFunctions列出了Calculator类的所有直接声明的函数。这意味着它也不研究超类。find选择divide()multiply()

  • 从函数中,我们通过.valueParameters遍历参数。对于每个参数,我们可以看到它是否有关联的注解NotZero。如果是,我们检查实际参数,如果是0.0,我们抛出一个异常。

  • 如果没有抛出异常,我们调用函数。arrayOf()表达式将接收器对象和函数参数连接成一个Array<Any>

@NotZero注解确保在调用Calculator.operator()时检查参数。要使用计算器,您可以这样写:

Calculator().
    operator(Calculator.Operator.DIVIDE,
            1.0, 1.0)

要查看注解是否有效,请使用第二个参数0.0尝试另一个调用。

练习 1

对于Calculator示例,添加一个新的注解@NotNegative和一个新的平方根操作sqrt()。请确保此运算符不允许使用负参数。注:实际平方根通过java.lang.Math.sqrt()计算。

十五、使用 Java 和 Kotlin APIs

Kotlin 有一个语言内核,可以处理类、对象、属性、函数、结构构造等等。到目前为止,我们已经讨论了很多。我们偶尔会提到并使用术语 Kotlin 标准库,但没有明确说明它实际上是什么。在现实生活中,图书馆是一个可以获得大量信息的地方。无论什么时候你需要知道一些事情,你都可以去那里,试着找一本书,告诉你事情是什么或者它们是如何工作的,或者你必须做些什么来实现一些事情。对于一门计算机语言来说,一个是类似的东西:一个有很多类和函数的存储库,你可以用它们来完成特定的任务。我们已经讨论过集合,集合由库类和函数管理。

API 与库携手并进。API 更关注库的外部;也就是说,如何从外部使用一个库,而不必了解其内部功能。

你能想到的库的例子有很多;例如,数学、化学、物理、生物学、社会学、加密标准、web 服务、用户界面、声音处理和图形,仅举几个例子,而写一本关于所有这些的书是不可能的。不过,区分 Kotlin 附带的基本库和可以按需添加的外部库是有意义的。仅仅查看内置库是一个更可行的任务,在这一章中,我们将查看 Kotlin 附带的库。

注意,在这样一本书里列出一个库必须提供的所有类和函数是不可能的,也是不可取的。除了非常简单的库之外,任何库中都有太多的。然而,我们可以尝试描述这些库,展示如何使用它们,并列出最重要的类和函数。这发生在随后的章节。

Kotlin 和 Java 库

在我们开始研究不同的 API 之前,我们需要讨论一下 Kotlin 库的来源。Kotlin 位于 JVM 之上,Kotlin 开发人员做了很好的工作,允许 Kotlin 和 Java 之间轻松的互操作。这包括使用 Java APIs 和库的能力。Java 已经存在了 20 多年,不难想象已经有了一些非常好的 Java 库,Kotlin 没有必要重做一切。Kotlin 所做的是包含一些已经包含在 Java 发行版中的库,然后使用它的类扩展机制在几个地方扩展或重新定义它们。

使用在线资源

对于 Kotlin 中包含的任何 API,拥有官方 API 文档总是一个好主意。去的地方是 https://kotlinlang.org/ 。在那里你会找到一个学习链接,让你到语言和标准库参考手册。如果这个链接过时了,在你喜欢的搜索引擎里搜索“kotlin 编程语言”就能找到。

正如已经指出的,Kotlin 与 Java 有很强的关系;将 Java 标准模块合并到 Kotlin 中特别容易。Android 平台包括各种 Java APIs,如果使用 Android Studio 进行开发,您不必做任何事情就可以使用它们。我们在本书中一直使用的 API level 28 具有以下来自 Java 8 的 Java APIs:

  • java.beans

  • java.io

  • java.lang

  • java.math

  • java.net

  • Java . 9 版

  • java.security

  • java.sql

  • java.text

  • java.time

  • java.util

  • javax.crypto

  • javax 微版 khronos

  • javax.net

  • javax .安全性

  • javax.sql

  • javax.xml

在 Oracle 网站上,您可以找到 Java 库的 API 文档。这个链接更准确地说是 https://docs.oracle.com/javase/8/docs/api/ ,但是如果这个链接过时了,在网上搜索“java 8 api”会很容易把你带到这些页面。

对于我们在以下章节中描述的 API,我们不再考虑它们是来自 Kotlin 还是 Java。如果您感兴趣,通过查看import语句,通常很容易看出这些类和接口来自哪里。如果它们以java.javax.开头,那么类和接口来自 Java,否则它们来自 Kotlin。

制作文档的本地副本

在 Android Studio 中,一旦你在任何类或接口名称上按下 Ctrl+B,你将被带到 Java 或 Kotlin 源代码。如果您第一次这样做,Android Studio 可能需要从互联网上下载源代码,但之后您会将源代码本地存储在您的 Android Studio 安装中。

如果您想在您的 PC 上拥有 API 文档的本地副本,那么对于 Java 来说,在 Oracle 下载网站上很容易找到相应的链接。对于 Kotlin,进入 https://github.com/JetBrains/kotlin/releases ,选择一个版本,然后下载源代码作为压缩存档。

你也可以从你的 Android 工作室获取源代码。确保通过在任何 Kotlin 标准库类上按 Ctrl+B 来下载源代码,然后转到STUDIO-INST/plugins/Kotlin/kotlinc/lib。在那里你会找到一个文件kotlin-stdlib-sources.jar。这是一个 ZIP 存档。您可以从存档中提取所有文件,并将其保存在 PC 上的任何位置。

十六、集合 API

我们已经在第九章中讨论了集合,即列表、集合和映射。然而,集合 API 是广泛的,包含了比我们在第九章中描述的更多的类和接口。对于 Java,API 甚至被称为集合框架。在这一章中,我们不要求详尽无遗,我们修改我们已经知道的,也讨论一些更有趣的集合接口、类和函数。

不幸的是,没有什么像java.collections包。关于 Java,集合 API 是分散的,其主要部分位于java.util包中。

注意

在本章中,我们将展示一种指定泛型类型参数的方法。在显而易见的地方,为了简洁起见,它们没有被示出。在所有情况下,我们使用E作为列表或集合的元素类型,使用KV作为映射的键和值。

接口

尽管 Java 已经有了集合、列表和映射的接口,Kotlin 也有自己的接口。这主要源于 Kotlin 需要区分可变和不可变的集合和映射。对于大多数用例,您可以只使用 Kotlin 版本,如果您尝试使用 Java 变体,编译器甚至会警告您。不过,也不禁止使用 Java 变体,这样做可能有原因。表 16-1 提供了一个概述。

表 16-1。

集合接口

|

连接

|

描述

| | --- | --- | | kotlin.collections.Iterable | iterable,或者可以在循环中迭代的东西。任何 iterable 都可以在一个for( x in a )循环中使用,所以如果你提供了自己的实现这个接口的类,你就可以在循环中使用它。所有集合(即列表和集合)都是可迭代的。 | | kotlin.collections.MutableIterable | 与Iterable相同,但是另外支持移除当前迭代的元素。 | | java.lang.Iterable | iterable 的 Java 变体;除非有充分的理由,否则不要使用它。 | | kotlin.collections.Collection | 一个通用的不可变集合接口。这是Iterable的一个子接口。 | | kotlin.collections.MutableCollection | 一个通用的可变集合接口。这是MutableIterable的一个子接口。 | | java.util.Collection | 集合接口的 Java 变体;除非有充分的理由,否则不要使用它。 | | java.util.Deque | 两头排队。使用它来实现或使用队列或堆栈。你可以把元素放在开头和结尾,两边都可以读取和撤回元素。deques 可用的函数数量有点多;通常情况下,您可以对以下设置感到满意:size() : Int得到尺寸。addFirst(element:E)向队列头添加一个元素。addLast(element:E)向队列尾部添加一个元素。removeFirst() : E获取并删除队列头的元素(如果队列为空,抛出异常)。removeLast() : E获取并删除队列尾部的元素(如果队列为空,抛出异常)。getFirst() : E检索但不删除队列头的元素(如果队列为空,则抛出异常)。getLast() : E检索但不删除队列尾部的元素(如果队列为空,则抛出异常)。对此没有 Kotlin 变体;德克总是易变的。 | | java.util.Queue | 一端排队的队伍。通常你可以使用一个双端的队列来代替。对此没有 Kotlin 变体;队列总是可变的。 | | kotlin.collections.List | 不可变列表。 | | kotlin.collections.MutableList | 可变列表。 | | java.util.List | 列表的 Java 变体;除非有充分的理由,否则不要使用它。 | | kotlin.collections.Set | 不变的集合。 | | kotlin.collections.MutableSet | 可变集合。 | | java.util.Set | 集合的 Java 变体;除非有充分的理由,否则不要使用它。 | | java.util.SortedSet | 其元素按自然排序顺序排列的集合。 | | java.util.NavigableSet | 一个可以在两个方向上迭代的SortedSet。 | | kotlin.collections.Map | 不变的地图。 | | kotlin.collections.MutableMap | 可变地图。 | | java.util.Map | 地图的 Java 变体;除非有充分的理由,否则不要使用它。 | | java.util.SortedMap | 其关键字按自然排序顺序排序的地图。 | | java.util.NavigableMap | 一个可以在两个方向上迭代的SortedMap。 |

请注意,所有这些接口都有必须在尖括号之间指定的泛型类型,除非 Kotlin 编译器可以推断出这些类型。对于地图,我们需要两个类型参数;其他人都需要一个。

仔细观察这个表,您可能会注意到两个有点奇怪的结构:一个以SortedSet形式表示的排序集合和一个以SortedMap形式表示的排序映射。这些语言结构在某些情况下会有所帮助,但在数学中却没有直接的对应。在数学中,集合和地图都是无序的!在你的代码中,最好不要在可以避免的地方使用它们。如果使用它们,算法不应该强烈依赖元素的顺序。这当然是个人喜好问题;把它当作一个暗示或建议。

班级

表 16-2 列出了实现集合和映射接口的类。

表 16-2。

集合类

|

班级

|

描述

| | --- | --- | | kotlin.collections.ArrayList | 可变和不可变列表的列表实现。 | | java.util.ArrayList | 除非你有充分的理由,否则不要使用它的 Java 变体。 | | kotlin.collections.HashSet | 可变和不可变集合的集合实现。 | | java.util.HashSet | 一个HashSet的 Java 变种;除非有充分的理由,否则不要使用它。 | | kotlin.collections.LinkedHashSet | 可变和不可变集合的集合实现。因为集合元素相互链接,所以迭代顺序与插入顺序相同。 | | java.util.LinkedHashSet | 一个LinkedHashSet的 Java 变种;除非有充分的理由,否则不要使用它。 | | kotlin.collections.HashMap | 可变和不可变映射的映射实现。 | | java.util.HashMap | 一个HashMap的 Java 变种;除非有充分的理由,否则不要使用它。 | | kotlin.collections.LinkedHashMap | 可变和不可变映射的映射实现。因为地图元素相互链接,所以迭代顺序与插入顺序相同。 | | java.util.LinkedHashMap | 一个LinkedHashMap的 Java 变种;除非有充分的理由,否则不要使用它。 | | java.util.ArrayDeque | 一个Deque实现。 | | java.util.EnumSet | 枚举元素的专用java.util.Set实现。 | | java.util.LinkedList | 一个带有链表元素的java.util.List实现。 | | java.util.PriorityQueue | 一个java.util.Queue实现,根据元素的自然顺序或根据构造期间传入的比较器定义的顺序,在某个位置插入元素。 | | java.util.Stack | java.util.List的后进先出(LIFO)实现。 | | java.util.TreeSet | 一个java.util.Set实现,其中的元素按照它们的自然顺序进行排序,或者按照构造期间传入的比较器进行排序。 | | java.util.concurrent.ArrayBlockingQueue | 一种固定大小的队列(先进先出表)。如果试图在队列已满时添加元素或试图在队列为空时移除元素,则阻止这两种情况。 | | java.util.concurrent.ConcurrentLinkedDeque | 允许对元素进行并发访问的 deque 实现。 | | java.util.concurrent.ConcurrentLinkedQueue | 允许对元素进行并发访问的队列实现。 | | java.util.concurrent.ConcurrentSkipListSet | 允许对元素进行并发访问的NavigableSet实现。 | | java.util.concurrent.CopyOnWriteArrayList | 允许对元素进行并发访问的java.util.List实现。每次写操作都会产生完整列表的新副本。 | | java.util.concurrent.CopyOnWriteArraySet | 允许对元素进行并发访问的java.util.Set实现。每个写操作都会产生完整集合的新副本。 | | java.util.concurrent.DelayQueue | 一个java.util.Queue实现,其中元素必须是java.util.concurrent.Delayed的子类。仅当延迟到期时,才允许删除元素。 | | java.util.concurrent.LinkedBlockingQueue | 可选地具有固定大小的队列(先进先出列表)。如果试图在队列已满时添加元素或试图在队列为空时移除元素,则阻止这两种情况。 | | java.util.concurrent.PriorityBlockingQueue | 一个java.util.Queue实现,根据元素的自然顺序或根据构造期间传入的比较器定义的顺序,在某个位置插入元素。可能会阻止检索操作,直到元素可用。 | | java.util.concurrent.SynchronousQueue | 一个java.util.Queue实现,其中插入操作只有在元素被并发请求时才是可能的。否则,插入操作会阻塞并等待。 |

注意,在属性声明中,通常希望为属性类型使用一个接口,但只为实例化使用一个类。这样我们表达的是属性做了什么,而不是它是如何做的。

var l:MutableList<String> = ArrayList()
// ... = ArrayList<String>() is unnecessary, because
// Kotlin can infer the type.

发电机功能

Kotlin 在自己的集合类中提供了数百个函数,为 Java 的集合类添加了扩展函数,此外还为我们提供了许多顶级函数。这一节和接下来的几节列出了 Kotlin 和 Java 中最重要的集合函数,但并不详尽。

表 16-3 显示了可以用来创建集合的顶级生成器函数。除非另有说明,否则返回的集合和映射只是包kotlin.collections中的类的实例。

表 16-3。

集合生成器

|

功能

|

描述

| | --- | --- | | emptyList<E>() | 创建给定元素类型的不可变空列表。 | | listOf<E>(...) | 创建作为参数给定的元素的不可变列表;例如,listOf(1, 2, 3) | | mutableListOf<E>(...) | 创建作为参数给出的元素的可变列表;例如,mutableListOf(1, 2, 3) | | listOfNotNull<E>(...) | 创建作为参数给定的元素的不可变列表,但是过滤掉null有值的参数;例如,listOfNotNull(1, 2, null, 3) | | List<E>(size: Int, init: (index: Int) -> E) | 创建由 lambda 函数计算的不可变列表,该函数作为第二个参数给出。请注意,尽管名称以大写字母开头,但这是一个函数。 | | MutableList<E>(size: Int,init: (index: Int) -> E) | 创建一个可变列表,由 lambda 函数计算,作为第二个参数给出。请注意,尽管名称以大写字母开头,但这是一个函数。 | | emptySet<E>() | 创建一个不可变的空集。 | | setOf<E>(...) | 创建作为参数给定的不可变元素集;例如,setOf(1, 2, 3) | | mutableSetOf<E>(...) | 创建作为参数给定的可变元素集;例如,mutableSetOf(1, 2, 3) | | emptyMap<K,V>() | 创建一个不可变的空映射。 | | mapOf<K,V>() | 创建作为参数给出的Pair元素的不可变映射;例如,mapOf(1 to "A", 2 to "B") | | mutableMapOf<K,V>(...) | 创建作为参数给出的Pair元素的可变映射;例如,mutableMapOf(1 to "A", 2 to "B") |

对于 Kotlin 来说通常是这样,如果 Kotlin 可以推断类型,那么可以省略类型参数。所以你可以写

listOf(1, 5, 7, 9)

而 Kotlin 知道这是一个List<Int>

集合和地图设置器和移除器

表 16-4 向你展示了如何向可变集合或映射添加元素,以及如何移除它们。

表 16-4。

集合变异函数

|

|

功能

|

描述

| | --- | --- | --- | | 列表,集合 | add(element:E) | 在列表末尾添加一个元素,或将一个元素添加到集合中。 | | 列表 | set(index:Int, element:E) | 覆盖给定索引处的元素。要覆盖的元素必须存在。 | | 列表 | list[index] = value | 同set() | | 列表,集合 | addAll(elements: Collection<E>) addAll(elements: Array<out E>) | 将作为参数提供的数组或集合中的所有元素添加到列表的尾部,或将元素添加到集合中。 | | 地图 | put(key:K, value:V) | 将键/值对放入映射中。如果该键已经存在,该值将被覆盖。 | | 地图 | map[key:K] = value:V | 同put()。 | | 地图 | putIfAbsent(key:K, value:V) | 将键/值对放入映射中,但前提是键以前不存在。 | | 地图 | set(key:K, value:V) | 同put()。 | | 地图 | putAll(from: Map<out K,V>) | 对作为函数参数提供的地图中的所有元素执行put()。 | | 列表,集合 | remove(element:E) | 从集合或列表中移除给定的元素。 | | 列表,集合 | removeIf { (E) -> Boolean } | 移除 lambda 函数返回true的所有元素。如果至少删除了一个元素,则返回true。 | | 列表,集合 | removeAll( elements:Collection<E>) removeAll(elements:Array<out T>) | 从列表或集合中移除包含在所提供的集合或数组参数中的所有元素。 | | 列表,集合 | removeAll { (E) -> Boolean } | 同removeIf()。 | | 地图 | remove(key:K) | 移除给定键处的元素(如果存在)。返回前一个值,如果不存在则返回null。 | | 地图 | remove(key:K, value:V) | 如果元素存在并具有给定值,则移除给定键处的元素。如果元素被移除,则返回true。 | | 列表,集合 | retainAll( elements:Collection<E>) | 更改给定的集合或列表,并使其仅保留那些也在给定的参数集合内的元素。 | | 地图、列表、集合 | clear() | 移除所有元素。 |

确定性吸气器

表 16-5 中列出了从集合和映射中检索元素的确定性 getters。

表 16-5。

吸气剂

|

|

功能

|

描述

| | --- | --- | --- | | 列表 | get(index:Int) | 检索指定索引处的元素。 | | 列表 | getOrNull(index:Int) | 检索指定索引处的元素,如果索引越界,则检索null。 | | 列表 | list[index:Int] | 同get()。 | | 列表 | first() | 返回第一个元素。 | | 目录 | firstOrNull() | 返回第一个元素,如果列表为空,则返回null。 | | 列表 | last() | 返回最后一个元素。 | | 列表 | lastOrNull() | 返回最后一个元素,如果列表为空,则返回null。 | | 列表,集合 | random() | 从列表或集合中返回一个随机元素。 | | 地图 | get(key:K) | 返回给定键的值,如果不存在则返回null。 | | 地图 | map[key:K] | 同get()。 | | 地图 | getOrDefault(key:K, defaultValue:V) | 返回给定键的值,如果不存在则返回defaultValue。 | | 地图 | getOrElse(key:K, defaultValue: (K) -> V) | 返回给定键的值,如果该值不存在,则返回作为第二个参数提供的 lambda 函数的结果。 | | 地图 | getOrPut(key:K, defaultValue: () -> V) | 返回键key的值。但是,如果键还不存在,调用 lambda 函数并将结果作为该键的值放入映射中。在后一种情况下,返回新值。 | | 列表,集合 | single() | 如果内部只有一个元素,则检索单个元素。否则将引发异常。 | | 列表,集合 | singleOrNull() | 如果内部只有一个元素,则检索单个元素。否则返回null。 | | 列表 | drop(n:Int) | 返回一个不可变的列表,其中包含原始列表中的元素,并删除前n个元素。 | | 列表 | dropLast(n:Int) | 返回一个不可变列表,其中包含原始列表中的元素,并从末尾删除了n个元素。 | | 列表 | slice(indices:IntRange) | 返回一个不可变列表,其中包含 range 参数给定索引处的元素。 | | 列表 | take(n:Int) | 返回一个不可变列表,其中包含原始列表中的第一个n元素。 | | 列表 | takeLast(n:Int) | 返回一个不可变列表,其中包含原始列表中的最后一个n元素。 |

集合和地图特征

采集和映射特性见表 16-6 。

表 16-6。

特征

|

接收器

|

功能

|

描述

| | --- | --- | --- | | 地图、列表、集合 | Size | 集合或地图的大小。 | | 地图、列表、集合 | count() | 同size。 | | 地图、列表、集合 | isEmpty() | 如果为空,则返回true。 | | 地图、列表、集合 | isNotEmpty() | 如果不为空,则返回true。 | | 列表,集合 | count((E) -> Boolean) | 计算给定谓词 lambda 函数返回的元素数true。 | | 地图 | count((K,V) -> Boolean) | 计算给定谓词 lambda 函数返回的元素数true。 | | 列表,集合 | indices | 作为IntRange的有效索引。 | | 列表 | lastIndex | 最后一个有效索引。 |

遍历集合和地图

对于遍历集合和地图,您可以使用表 16-7 中所示的结构之一。

表 16-7。

横越

|

|

建造

|

描述

| | --- | --- | --- | | 列表、设置、实现Iterable | for( i in x ) { ... } | 一个语言结构,i是循环变量并接收元素。 | | 地图 | for( me in x ) { ... } | 一个语言结构,me是循环变量,接收Map.Element<K,V>元素。您可以通过me.key获取密钥,通过me.value获取值 | | 地图 | for( (k,v) in x ) { ... } | 语言结构,kv是循环变量,接收每个 map 元素的键和值。 | | 列表、设置、实现Iterable | x.forEach { i -> ... } | 遍历x的所有元素,i接收每个元素。 | | 列表、设置、实现Iterable | x.onEach { i -> ... } | 与forEach()相同,但之后返回迭代列表、集合或 iterable。 | | 列表、设置、实现Iterable | x.forEachIndexed { ind, i -> ... } | 遍历x的所有元素,i接收每个元素。Ind是指数变量(0,1,2,...). | | 地图 | x.forEach { me -> ... } | 遍历地图xme的所有元素,类型为Map.Element<K,V>。您可以通过me.key获取密钥,通过me.value获取值 | | 地图 | x.forEach { k,v -> ... } | 遍历 map 的所有元素xk是键,v是每个元素的值。 |

转换

将一个集合或地图转换成另一个集合或地图的可能性是无限的。表 16-8 显示了从地图中提取关键字和值的函数。

表 16-8。

提取关键字和值

|

建造

|

返回

|

描述

| | --- | --- | --- | | map.keys | MutableSet<K> | 作为一个集合从地图中获取关键字。 | | map.values | MutableCollection<V> | 将地图中的值作为集合获取。 |

在本节中,我们分别使用listsetmapcolliter来表示ListSetMapCollectionIterable类型的变量。请记住,列表或集合是一个集合,任何集合都是可迭代的。

表 16-9 显示了在逐个元素的基础上转换集合或映射元素的各种函数。

表 16-9。

变换:贴图

|

建造

|

返回

|

描述

| | --- | --- | --- | | iter.map(transform: (E) -> R) | List<R> | 根据给定的 lambda 函数转换所有集合或任何其他 iterable 的条目。返回一个不可变列表。 | | iter.mapIndexed( transform: (Int, E) -> R) | List<R> | 根据给定的 lambda 函数转换所有集合或任何其他 iterable 的条目。lambda 函数获取索引(0,1,2,...)作为它的第一个参数。返回一个不可变列表。 | | map.map(transform: (Map.Entry<K,V>) -> R) | List<R> | 根据提供给每个 map 元素的 lambda 函数的结果创建一个新的不可变列表。 | | map.mapKeys( transform: (Map.Entry<K,V>) -> R)) | Map<R,V> | 使用从提供的 lambda 函数派生的键创建一个新的不可变映射。 | | map.mapValues( transform: (Map.Entry<K, V>) -> R)) | Map<K,R> | 使用从提供的 lambda 函数派生的值创建一个新的不可变映射。 |

有关更改列表排序顺序或将列表或集合转换为排序列表的功能描述,请参见表 16-10 。

表 16-10

变换:重新排序

|

建造

|

返回

|

描述

| | --- | --- | --- | | list.asReversed() | List<E>MutableList<E> | 反转列表的迭代顺序,而不更改列表。保持原始列表的可变性。请注意,对结果列表的更改会反映在原始列表中。 | | list.reverse() | Unit | 就地反转可变列表。原始列表会发生变化。 | | iter.reversed() | List<E> | 返回一个新的不可变列表,其中原始集合或 iterable 中的元素的排序顺序相反。 | | iter.distinct() | List<E> | 返回一个新的不可变列表,该列表只包含原始集合或 iterable 中的不同元素。 | | iter.distinctBy( selector: (E) -> K) | List<E> | 返回一个新的不可变列表,该列表只包含原始集合中的不同元素。对于等式检查,使用来自所提供的 lambda 函数的结果。 | | list.shuffle() | Unit | 将可变列表中的元素随机打乱。 | | iter.shuffled() | List<E> | 返回一个不可变列表,其中包含原始集合或 iterable 中随机打乱的元素。 | | list.sort() | Unit | 根据自然排序顺序对可变列表进行就地排序。这些元素必须实现Comparable接口。sortDescending()函数以相反的顺序排序。 | | list.sortBy(selector: (E) -> R?) | Unit | 根据选择器结果的自然排序顺序对可变列表进行就地排序。选择器结果必须实现Comparator接口。sortDescending()函数以相反的顺序排序。 | | iter.sorted() | List<E> | 返回一个新的不可变列表,其中的元素按照自然排序顺序排序。这些元素必须实现Comparable接口。sortedDescending()函数以相反的顺序排序。 | | iter.sortedBy( selector: (E) -> R?) | List<E> | 返回一个新的不可变列表,其中的元素按照选择器结果的自然排序顺序排序。选择器结果必须实现Comparable接口。sortedByDescending()函数以相反的顺序排序。 | | list.sortWith( comparator: Comparator<in E> | Unit | 根据作为参数给出的比较器对可变列表进行就地排序。选择器结果必须实现Comparator接口。sortDescending()函数以相反的顺序排序。 | | iter.sortedWith( comparator: Comparator<in E> | List<E> | 返回一个新的不可变列表,其中的元素根据作为参数给出的比较器进行排序。 |

有几个函数可以用来收集子列表或子映射的元素;即作为列表或映射元素的列表和映射(见表 16-11 )。

表 16-11

变换:展平

|

建造

|

返回

|

描述

| | --- | --- | --- | | iter.flatten(...) | List<E> | 这里的iter是一个Iterable<Iterable<E>>,例如,包含集合的集合就是这种情况。返回一个新的不可变列表,其中所有元素都连接在一个列表中。 | | iter.flatMap( transform: (E) -> Iterable<R>) | List<R> | 将 transform 函数应用于原始集合或 iterable 中的所有元素,并返回一个类似于 list 或 set 的Iterable,返回一个单一的不可变列表,其中所有转换结果元素串联在一起。 | | map.flatMap( transform: (Map.Entry<K,V>) -> Iterable<R>) | List<R> | 将 transform 函数应用于原始 map 中的所有元素,并返回一个类似于 list 或 set 的Iterable,返回一个不可变的列表,将所有转换结果元素连接在一起。 |

通过将元素与新映射的键或值相关联,可以将列表和集合转换为映射。表 16-12 显示了这样的关联函数。

表 16-12

转换:关联

|

建造

|

返回

|

描述

| | --- | --- | --- | | iter.associate( transform: (E) -> Pair<K, V>) | Map<K,V> | 给定输入列表或集合或 iterable,transform lambda 函数应该返回一个用于返回的 map 中的新元素的Pair<K,V>。 | | iter.associateBy( keySelector: (E) -> K) | Map<K,E> | 给定输入列表或集合或 iterable,keySelector lambda 函数用于为返回的 map 中的新元素创建一个键。该值是原始元素。 | | iter.associateBy( keySelector: (E) -> K, valueTransform: (E) -> V ) | Map<K,V> | 给定输入列表或集合或 iterable,keySelector lambda 函数用于为返回的 map 中的新元素创建一个键。这些值将取自valueTransform调用结果。 | | iter.associateWith( valueSelector: (E) -> V) | Map<E,V> | 给定输入列表或集合或 iterable,valueTransform lambda 函数用于为返回的 map 中的新元素创建值。原始元素作为一个键被使用。 |

练习 1

给出一个类data class Employee(val lastName:String, val firstName:String, val ssn:String)和一个列表

val l = listOf(
    Employee("Smith", "Eve", "012-12-5678"),
    Employee("Carpenter", "John", "123-06-4901"),
    Employee("Cugar", "Clara", "034-00-1111"),
    Employee("Lionsgate", "Peter", "965-11-4561"),
    Employee("Disney", "Quentin", "888-12-3412")
)

从 SSN 排序的列表中获取一个新的不可变列表。

练习 2

给定练习 1 中的员工列表,创建一个不可变的映射,将 SSN 映射到员工。

练习 3

的产量是多少

listOf(listOf(1, 2), listOf(3, 4)).flatten()

练习

的产量是多少

listOf(listOf(1, 2), listOf(3, 4)).
      flatMap { it.map { it.toString() }    }

过滤

与转换密切相关的是过滤功能。它们用于根据某种标准获得新的集合或地图。表 16-13 列出了过滤功能。

表 16-13

过滤

|

功能

|

描述

| | --- | --- | | iter.filter( predicate: (E) -> Boolean) | 返回一个新的不可变列表,只包含那些匹配给定谓词的元素。 | | iter.filterNot( predicate: (E) -> Boolean) | 返回一个新的不可变列表,只包含那些与给定谓词不匹配的元素。 | | iter.filterIndexed( predicate: (index:Int, T) -> Boolean) | 返回一个新的不可变列表,只包含那些匹配给定谓词的元素。lambda 函数检索索引(0,1,2,...)作为第一个参数。 | | map.filter( predicate: (Map.Entry<K,V>) -> Boolean) | 返回一个新的不可变映射,只包含那些匹配给定谓词的元素。 | | map.filterNot( predicate: (Map.Entry<K,V>) -> Boolean) | 返回一个新的不可变映射,只包含那些与给定谓词不匹配的元素。 |

在本节中,我们分别使用listsetmapcolliter来表示ListSetMapCollectionIterable类型的变量。请记住,列表或集合是一个集合,任何集合都是可迭代的。

练习 5

给定练习 1 中的雇员列表,创建一个新的不可变列表,只包含以 0 开头的 SSN。提示:String.startsWith(...)检查字符串是否以某些字符开头。

改变可变性

你可以在表 16-14 中看到,可变映射和列表的转换通常会返回不可变的映射或集合。如果您需要一个可变的地图或集合,Kotlin 可以帮助您。

表 16-14

改变模式

|

功能

|

描述

| | --- | --- | | list.toMutableList() | 将不可变列表转换为可变列表。 | | set.toMutableSet() | 将不可变集合转换为可变集合。 | | map.toMutableMap() | 将不可变映射转换为可变映射。 | | mutableList.toList() | 将可变列表转换为不可变列表。 | | mutableSet.toSet() | 将可变集合转换为不可变集合。 | | mutableMap.toMap() | 将可变映射转换为不可变映射。 |

元素检查

要检查集合或地图的任何或所有元素是否满足某个标准,您可以使用表 16-15 中描述的函数之一。在本节中,我们分别使用listsetmapcolliter来表示ListSetMapCollectionIterable类型的变量。请记住,列表或集合是一个集合,任何集合都是可迭代的。

表 16-15

检查

|

功能

|

描述

| | --- | --- | | iter.any(predicate: (E) -> Boolean) | 如果任何元素满足谓词,则返回true。 | | iter.all(predicate: (E) -> Boolean) | 如果所有元素都满足谓词,则返回true。 | | iter.none(predicate: (E) -> Boolean) | 如果没有元素满足谓词,则返回true。 | | map.any(predicate: (Map.Entry<K,V>) -> Boolean) | 如果任何元素满足谓词,则返回true。 | | map.all(predicate: (Map.Entry<K,V>) -> Boolean) | 如果所有元素都满足谓词,则返回true。 | | map.none(predicate: (Map.Entry<K,V>) -> Boolean) | 如果没有元素满足谓词,则返回true。 |

练习 6

为列表listOf(1, 2, 3, 4)创建一个检查,查看是否所有元素都大于0

查找元素

为了从一个集合或地图中找到特定的元素,你可以使用表 16-16 中显示的函数之一,我们也在其中添加了包含检查。

表 16-16

发现

|

|

功能

|

描述

| | --- | --- | --- | | 列表,可重复项 | indexOf(element:E) | 确定列表或 iterable 中元素的索引(Int),如果找不到该元素,则确定1。 | | 列表,可重复项 | find(predicate: (e) -> Boolean) | 返回谓词 lambda 函数返回true的第一个元素,如果没有匹配的元素,则返回null。 | | 列表,可重复项 | findLast(predicate: (e) -> Boolean) | 返回谓词 lambda 函数返回true的最后一个元素,如果没有匹配的元素,则返回null。 | | 列表 | binarySearch( element: E?, fromIndex: Int = 0, toIndex: Int = size) | 在列表中执行快速二分搜索法。列表必须根据元素的自然顺序进行排序,因此必须实现Comparable接口。如果找到元素,则返回索引,或者返回−insertion_point-1,其中insertion_point是元素将被插入的索引,以保持列表的排序顺序。 | | 列表 | binarySearch( element: E?, comparator: Comparator<in E>, fromIndex: Int = 0, toIndex: Int = size) | 与binarySearch()相同,但使用为比较元件提供的比较器。 | | 列表、集合、可重复项 | contains(element: E) | 如果列表、集合或 iterable 包含指定的元素,则返回true。 | | 地图 | contains(key: K) | 如果映射包含指定的键,则返回true。 | | 地图 | containsKey(key: K) | 与地图的contains()相同。 | | 地图 | containsValue(value: V) | 如果映射包含指定的值,则返回true。 |

练习 7

给定一个包含Int s 的列表l,找到一种不使用if的单表达式方式,如果列表包含42则抛出异常。提示:使用find()contains(),也可能是takeIf()?.run

聚集、折叠和缩减

聚合器从集合中推导出总和、最大值、最小值或平均值。这些在表 16-17 中列出。

在本节中,我们分别使用listsetmapcolliter来表示ListSetMapCollectionIterable类型的变量。请记住,列表或集合是一个集合,任何集合都是可迭代的。

表 16-17

聚集

|

|

功能

|

描述

| | --- | --- | --- | | 一组数字(ByteShortIntLongFloatDouble) | sum() | 总结了元素。类型ByteShort产生一个Int值的和;所有其他元素都产生与元素相同的结果类型。 | | 任何集合或可迭代的 | sumBy( selector: (E) -> Int) | 对每个元素应用 lambda 函数后,对元素求和。产生一个Int号。 | | 任何集合或可迭代的 | sumByDouble( selector: (E) -> Double) | 对每个元素应用 lambda 函数后,对元素求和。产生一个Double号。 | | 一组数字(ByteShortIntLongFloatDouble) | average | 计算所有元素的平均值,作为一个Double. | | 实现Comparable的元素集合 | max() | 返回最大值。 | | 任何集合或可迭代的 | maxBy( selector: (E) -> R) | 返回应用selector后的最大值(必须返回一个Comparable)。 | | 任何地图 | maxBy( selector: (Entry<K, V>) -> R) | 返回应用selector后的最大值(必须返回一个Comparable)。 | | 任何集合或可迭代的 | maxWith( comparator: Comparator<in E>) | 根据提供的比较器返回最大值。 | | 任何地图 | maxWith( comparator: Comparator<in Map.Entry<K,V>) | 根据提供的比较器返回最大值。 | | 实现Comparable的元素集合 | min() | 返回最小值。 | | 任何集合或可迭代的 | minBy( selector: (E) -> R) | 返回应用selector后的最小值(必须返回一个Comparable)。 | | 任何地图 | minBy( selector: (Entry<K, V>) -> R) | 返回应用selector后的最小值(必须返回一个Comparable)。 | | 任何集合或可迭代的 | minWith( comparator: Comparator<in E>) | 根据提供的比较器返回最小值。 | | 任何地图 | minWith( comparator: Comparator<in Map.Entry<K,V>) | 根据提供的比较器返回最小值。 |

化简获取集合或可迭代对象的第一个元素,将其存储在一个变量中,然后对集合或可迭代对象中的所有其他元素重复应用一个操作。例如,如果运算是加法,最后你得到集合的和:

start with: (1, 2, 3)
take 1st element:              (1),      remains (2, 3)
take next element, apply "+":  (1+2),    remains (3)
take next element, apply "+":  (1+2+3),  done.
result is 1+2+3 = 6

从右开始的缩减以相反的顺序遍历集合;也就是说,它首先获取最后一个元素,将运算符应用于倒数第二个元素,依此类推。缩减功能如表 16-18 所示。

表 16-18

减低

|

功能

|

返回

|

描述

| | --- | --- | --- | | <S, E : S> iter<E>.reduce( operation: (acc: S, E) -> S) | S | 在集合或 iterable 上减少。操作 lambda 函数接收当前累加器值和当前迭代的元素。 | | <S, E : S> iter<E>.reduceIndexed( operation: (index: Int, acc: S, E) -> S) | S | 与reduce()相同,但是该操作另外接收当前迭代索引(0,1,2,...). | | <S, E : S> list<E>.reduceRight( operation: (E, acc: S) -> S) | S | 从右侧减少。请注意,这对 iterables 不起作用,因为没有什么像正确的迭代。 | | <S, E : S> list<E>.reduceRight- Indexed( operation: (index: Int, E, acc: S)-> S) | S | 与reduceRight()相同,但是该操作另外接收当前迭代索引(0,1,2,...). |

请注意,尽管迭代遍历了类型为E的元素,但操作函数也允许计算超类型E。这就是类型规范中的E : S所代表的意思。在这种情况下,累加器和总结果将与这个超类型具有相同的类型。

一个就是减的老大哥。缩减从集合或 iterable 的第一个元素开始,然后使用其余的元素来更新它,而折叠则使用一个专用的折叠累加器对象,该对象逐步接收所有迭代的元素,因此可以更新其状态。因为累加器对象可以有任何合适的类型,所以折叠比还原更强大。折叠功能列于表 16-19 中。

表 16-19

可折叠的

|

功能

|

返回

|

描述

| | --- | --- | --- | | iter.fold( initial: R, operation: (acc: R, E) -> R) | R | 在集合或 iterable 上折叠。第一个参数接收累加器对象。操作 lambda 函数接收当前累加器对象和当前迭代的元素。 | | iter.foldIndexed( initial: R, operation: (index:Int, acc: R, E) -> R) | R | 与fold()相同,但是该操作另外接收当前迭代索引(0,1,2,...). | | list.foldRight( initial: R, operation: (E, acc: R) -> R) | R | 折叠列表,从最后一个对象开始,以相反的顺序迭代。第一个参数接收累加器对象。操作 lambda 函数接收当前累加器对象和当前迭代的元素。 | | list.foldRightIndexed( initial: R, operation: (index:Int, E, acc: R) -> R) | R | 与foldRight()相同,但是该操作另外接收当前迭代索引(0,1,2,...). |

练习 8

给出一个类data class Parcel(val receiverId:Int, val weight:Double)和一个列表

val l = listOf( Parcel(1267395, 1.45),
    Parcel(1515670, 0.46),
    Parcel(8345674, 2.50),
    Parcel(3418566, 1.47),
    Parcel(3491245, 3.04)
)

不使用forwhile循环计算重量总和。

连接

有时,您需要的不是一个完整的折叠操作,即一个对象从迭代中接收所有元素,而是创建一个集合或 iterable 的字符串表示,将所有元素的字符串表示连接起来。虽然这可以通过fold()实现,但是 Kotlin 提供的一个专用连接函数有几个额外的特性,即一个前缀和一个后缀,以及一个限制和一个截断指示符。你用

fun <E> Iterable<E>.joinToString(
    separator: CharSequence = ", ",
    prefix: CharSequence = "",
    postfix: CharSequence = "",
    limit: Int = -1,
    truncated: CharSequence = "...",
    transform: (E) -> CharSequence = null
): String

集合或 iterable 上,具有以下特征:

  • 如果您指定了分隔符,此分隔符将用于分隔输出字符串中的各项。否则将使用,

  • 如果指定前缀,它将用作输出字符串的前缀。否则将不使用任何内容。

  • 如果指定了后缀,它将被用作输出字符串的后缀。否则将不使用任何内容。

  • 如果指定了限制,则用于构造输出字符串的元素数量将受到限制。否则将使用1,这表示没有限制。

  • 如果指定截断字符串,它将用于表示由于超出限制(如果给定)而导致的截断。否则将使用...

  • 如果指定一个转换函数,它将用于从每个元素创建一个字符串。否则将使用null,这意味着toString()将应用于每个元素。

分组

分组是基于某种标准将一个列表分割成子列表。设想一个雇员列表,每个雇员都有一个employer字段,您希望为每个雇主创建一个列表。通过编写几行代码,这并不难做到,但是因为这是一个重复的任务,所以有一些标准的库函数可以帮助我们。分组相关功能见表 16-20 。

表 16-20

分组

|

功能

|

返回

|

描述

| | --- | --- | --- | | <E, K> iter.groupBy( keySelector: (E) -> K) | Map<K, List<E>> | 基于由keySelector函数计算的密钥进行分组。 | | <E, K> iter.groupBy( keySelector: (E) -> K, valueTransform: (E) -> V) | Map<K, List<V>> | 基于由keySelector函数计算的键进行分组,但也通过valueTransform函数转换值。 | | <E, K> iter.groupingBy( keySelector: (E) -> K) | Grouping<E,K> | 基于由keySelector函数计算的关键字准备分组。创建一个特殊的Grouping对象,可用于进一步的操作。 | | grouping.aggregate( operation: (key: K, accumulator: R?, element: E, first: Boolean) -> R) | Map<K, R> | 获取来自groupingBy()的结果,并使用原始键构建地图。至于值,使用operation来累加每个组的值(例如,累加器可以是一个列表)。 | | grouping.eachCount() | Map<K, Int> | 返回每组元素计数的映射。 | | grouping.fold( initialValueSelector: (key: K, element: E) -> R, operation: (key: K, accumulator: R, element: E) -> R) | Map<K, R> | 获取来自groupingBy()的结果,并使用原始键构建地图。至于数值,使用operation累加各组的数值。每组的初始累加器由initialValueSelector函数构造。 |

在本节中,我们分别使用listsetmapcolliter来表示ListSetMapCollectionIterable类型的变量。请记住,列表或集合是一个集合,任何集合都是可迭代的。

拉链

如果你有两个相关的列表,想把它们放在一起,Kotlin 提供了一个压缩功能,可以帮助你。比方说,您有一个雇员列表和另一个尚未注册的一年的年薪列表。两个列表大小相同,每个索引指向一对匹配的雇员和薪水。在命令式编程风格中,您应该编写类似这样的代码来获取更新的员工列表:

class Employee {
    ...
    fun setSalary(year:Int, salary:Double) {}
}

val employees = ... // list
val newSalaries = ... // list
val newYear = 2018
val newEmployees = mutableListOf<Employee>()
for(ind in employees.indices) {
    val e = employees[ind]
    val sal = newSalaries[ind]
    e.setSalary(newYear, sal)
    newEmployees.add(e)
}

我们可以使用标准库提供的 zipping 函数以函数式的方式重写它:

<E, R> Iterable<E>.zip(
    other: Iterable<R>
): List<Pair<E, R>>

这给了我们:

val employees = ... // list
val newSalaries = ... // list
val newYear = 2018
val newEmployees = employees.zip(newSalaries).
    map{ p ->
      p.first.setSalary(newYear, p.second)
      p.first
    }

这里的zip()给出了一个Pair的列表,每个列表包含一个Employee和一份薪水(例如Double)。map()调查每一对,并相应地更新员工。

还有一个相反的操作,从一个列表创建两个列表,恰当地称为解压

<E, R> Iterable<Pair<E, R>>.unzip():
      Pair<List<E>, List<R>>

更准确地说,这是这种解压缩操作的第二部分;您将首先使用映射函数创建一个Pair列表;例如:

list.map { item ->
    Pair(item.something, item.somethingElse)
}.unzip()

开窗术

对于用户界面编程,你经常需要将一个列表分割成给定大小的块。例如,用户界面显示大小为 10 的块,并提供向前翻页和向后翻页按钮来显示较长列表的下一个或前一个块。为此,标准库提供了一个窗口功能(见表 16-21 )。

表 16-21

开窗术

|

功能

|

返回

|

描述

| | --- | --- | --- | | <E> iterable.windowed( size: Int, step: Int = 1, partialWindows: Boolean = false ) | List<List<E>> | 创建 iterable 或集合的窗口视图。每个块都有大小sizestep表示每个块的索引偏移量(通常设置step = size)。如果你想在最后允许更小的块,你必须将partialWindows设置为true。 | | <E, R> iterable.windowed( size: Int, step: Int = 1, partialWindows: Boolean = false, transform: (List<E>) -> R) | List<R> | 与windowed()相同,但是提供了一个transform函数来作用于每个块。 |

顺序

序列是延迟求值的集合。我们的意思是,除了从kotlin.collections包中收集数据之外,没有大量的数据保存在内存中。因此,如果您创建一个大小为 1,000,000 的集合,将有 1,000,000 项以对象引用或原语的形式分配到内存中。然而,一个大小为 1,000,000 的序列只是表明我们有一些东西可以迭代 1,000,000 次,而不需要所有与之相关的值。序列接口、类、函数都有自己的包:kotlin.sequences

序列暴露了很多我们从集合中已经知道的函数。您可以使用forEach()、应用过滤器、执行映射、使用缩减、执行折叠等等。这里我们没有全部展示;相反,我们列出了几个更重要的,让你开始。有关更多信息,请参考 Kotlin 文档。

要创建一个给定值列表的序列,可以使用sequenceOf()函数;例如:

sequenceOf(1, 2, 7, 5)

或者,你可以拿任何Iterable(集合或列表或范围,或任何集合)来写

iter.asSequence()

要创建不依赖于现有集合或数组的真正序列,有几种可能性。其中最简单的可能是使用函数generateSequence(),如

// Signature:
// fun <T : Any> generateSequence(
//     nextFunction: () -> T?
// ): Sequence<T>

var iterVar = 0
val seq = generateSequence {
    iterVar++
}

这里我们所要做的就是提供一个函数来生成下一个序列值。这种方法的缺点是我们有一个状态,即迭代属性iterVar,在generateSequence()周围的某个地方。这是干净代码的反模式思维。前来救援的是generateSequence()的另一个变种:

fun <T : Any> generateSequence(
  seed: T?,
  nextFunction: (T) -> T?
): Sequence<T>
// or
fun <T : Any> generateSequence(
  seedFunction: () -> T?,
  nextFunction: (T) -> T?
): Sequence<T>

在这里,我们可以直接或通过一个生成器函数提供一个种子,nextFunction() lambda 接收当前的迭代器值,并返回下一个迭代器值。一个非常简单的序列(0,1,2,...)这样写道

val seq = generateSequence(
  seed = 0,
  nextFunction = { curr -> curr + 1 }
)

// example usage:
seq.take(10).forEach { i ->
  // i will have values 0, 1, 2, ..., 9
  ...
}

迭代变量不一定是一个Int,甚至根本不是一个数字。作为一个例子,考虑斐波纳契数列112358,...其中每一项都是前两项的总和。这可以通过Pair来处理,顺序如下

val seqFib = generateSequence(
    seed = Pair(1,1),
    nextFunction = { curr ->
        Pair(curr.second, curr.first + curr.second)
    }
)

// example usage
seqFib.take(10).map { it.second }.forEach {
    Log.e("LOG", "fib: " + it)
}

nextFunction以一个pair(1,1)开始,接着是一个pair(1,2)pair(2,3)pair(3,5),以此类推。示例用法片段中的映射提取并显示每对中的第二个值。有趣的是,对于更高的数字,每对中第二个成员与第一个成员的比例接近于黄金比例 0.5 · (1 + \sqrt{5} ) = 1.6180339887:

val p = seqFib.take(40).last
val gr = p.second * 1.0 / p.first
// =  1.618033988749895

一种更灵活、更复杂的方法是使用另一个序列生成函数:sequence()。它的签名是

fun <T> sequence(
    block: suspend SequenceScope<T>.() -> Unit
): Sequence<T>

该函数实际上以如下方式实例化了一个kotlin.sequences.Sequence对象:

Sequence { iterator(block) }

其中iterator()创建并返回SequenceBuilderIterator的实例。lambda 函数前面的这个SequenceBuilderIteratorsuspend确保序列可以在并行执行环境中使用。我们将在本书的后面讨论并发执行。我们现在需要知道的是,由于具有接收器规范SequenceScope<T>.() -> Unit的λ,关于blockλ函数,我们在SequenceScope对象的环境中起作用。为了让这个构造做一些合理的事情,从内部block你必须至少调用一个

yieldAll([some implementation of Iterable])
// or
yieldAll([some implementation of Iterator])
// or
yieldAll([some implementation of Sequence])

作为一个例子,考虑这个:

val sequence = sequence {
    // This is an iterable:
    yieldAll(1..10 step 2)
}

// Usage example:
sequence.take(8).forEach {
    Log.e("LOG", it.toString())

}
// -> 1, 3, 5, 7, 9

经营者

对于 iterables,包括像集合和列表这样的所有集合,以及映射,有两个操作符,如表 16-22 所示,您可以使用它们中的两个来组合。

表 16-22

经营者

|

操作数

|

操作员

|

操作数

|

返回

| | --- | --- | --- | --- | | Iterable(集合、列表、集合) | intersect | Iterable(集合、列表、集合) | 创建一个新的不可变的Set,它包含两个操作数中包含的所有元素。 | | Iterable(集合、列表、集合) | union | Iterable(集合、列表、集合) | 创建一个新的不可变的Set,它包含一个或两个操作数中包含的所有元素。 | | Iterable(集合、列表、集合) | + | E | 返回一个新的不可变的List,将左操作数中的所有元素追加到右操作数中。 | | Iterable(集合、列表、集合) | + | 可迭代,数组,序列 | 返回一个新的不可变的List,将左操作数中的所有元素追加到右操作数中的所有元素。 | | Iterable(集合、列表、集合) | - | E | 返回一个新的不可变的List,包含左操作数中的所有元素,如果左操作数中存在右操作数,则减去右操作数。 | | Iterable(集合、列表、集合) | - | 可迭代,数组,序列 | 返回一个新的不可变的List,它包含左操作数中的所有元素,减去右操作数中也存在于左操作数中的所有元素。 | | 地图 | + | Pair<K,V> | 返回一个新的不可变映射,包含左操作数和右操作数的所有条目。如果该键以前存在,则该项会被覆盖。 | | 地图 | + | Iterable< Pair<K,V>>, Array<out Pair<K, V>>, Sequence< Pair<K,V>>, Map<out K, V> | 返回一个新的不可变映射,包含左操作数中的所有条目,以及右操作数中的所有元素。如果右操作数中的任何键也存在于左操作数中,则相应的条目会被右操作数覆盖。 | | 地图 | - | K | 返回一个新的不可变映射,其中包含左操作数的所有条目,但右操作数指定的键已被删除(如果存在)。 | | 地图 | - | Iterable<K>, Array<out K>, Sequence<K> | 返回一个新的不可变映射,其中包含左操作数的所有条目,但右操作数指定的所有键都被删除(仅针对左操作数中存在的键)。 |

因为很多其他的操作符像*/%等等都是未定义的,我们知道我们可以通过操作符重载来定义它们,你可以为集合和地图设计你自己的操作符来实现很多事情。只要确保你提供了好的文档,这样其他人就可以理解他们在做什么。