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

113 阅读41分钟

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

原文:Learn Kotlin for Android Development

协议:CC BY-NC-SA 4.0

四、类和对象:扩展功能

这一章涵盖了一些扩展的面向对象特性,这些特性对于一个程序来说并不是必须的,但是仍然可以提高可读性和表现力。本章假定你已经阅读了第二章。我们还使用第二章中的NumberGuess示例应用。

匿名类

在您的代码中,在某些情况下,您可能希望创建接口的一次性实现或某个类的一次性子类。在 Kotlin,可以写

class A : SomeInterface {
    // implement interface functions ...
}
val inst:SomeInterface = A()
// use inst ...

或者

open class A : SomeBaseClass() {
    // override functions ...
}
val inst:SomeBaseClass = A()
// use inst ...

在函数内部,有一种更简洁的方法来创建和使用这样的一次性实例:

val inst:SomeInterface = object : SomeInterface {
    // implement interface functions ...
}
// use inst ...

或者

val inst:SomeBaseClass = object : SomeBaseClass() {
    // override functions ...
}
// use inst ...

如果像后面的清单中那样扩展一些超类,这个超类也可能是抽象的。然而,随后有必要实现所有的抽象函数,这通常是实例化成为可能的情况。因为接口实现或子类的名字既没有指定也不需要,这样的类叫做匿名类。在花括号之间的类体中,你可以写任何你可以在命名类体内写的东西。

注意

声明中的object :表明只有一次实例化。匿名类不可能有多个实例。

我们知道this指的是实际的实例。匿名类内部也是如此,这里的this指的是匿名类的一个实例。有一个对this的扩展,它允许我们获得包围类的的实例:只需将@ClassName附加到this上。例如在

interface X {
    fun doSomething()
}
class A {
    fun meth() {
        val x = object : X {
            override doSomething() {
                println(this)
                println(this@A)
            }
        }
    }
}

第一个this指的是匿名类,this@A指的是类A的实例。

内部类

类和单例对象也可以在其他类或单例对象中声明,甚至可以在函数中声明。然后可以从它们的作用域内访问它们,所以如果一个类或单例对象在某个类A内被声明,它可以在A内被实例化。如果它是在函数内部声明的,那么它可以从声明开始一直使用到函数结束。

class A {
    class B { ... }
    // B can now be instantiated from
    // everywhere inside A
    fun meth() {
        ...
        class C { ... }
        // C can now be used until the end of
        // the function
        ...
    }
}

其他类或其他对象内部的类和对象可以使用类似于包的路径规范从外部寻址:如果在A(类或单例对象)内部声明了作为类或对象的X,那么可以编写A.X从外部访问它。然而,只有当内部类提供了某种到封闭类的接口时,您才应该这样做,以避免破坏封装原则。

class A {
    class B { ... }
}
fun main(args:Array<String>) {
    val ab = A.B()
    // do something with it ...
}

类外的函数和属性

在您的项目中,您可以拥有不包含单个classinterfaceobject声明,但显示valvar属性和功能的 Kotlin 文件。尽管乍一看,如果我们使用这样的文件,我们似乎是在面向对象之外工作,但实际上 Kotlin 编译器隐式地、秘密地基于名创建了一个 singleton 对象,并将这样的属性和功能放入这个对象中。

注意

对于非常小的项目,不使用显式类和单例对象是可以接受的。如果一个项目变得更大,仍然有可能只使用这样的非类文件,但是你最终会有混乱和不可读的代码的风险。

根据这一事实,以下规则适用于此类属性和功能:

  • 在文件的什么地方声明valvar属性和函数并不重要;它们在文件的任何地方都是可用的。

  • 这样的属性和函数对你用import the.package.name.name写在其他文件中的其他类或单例对象是可见的,其中最后的name指的是属性或函数名。

  • 一个包中可以有几个这种类型的文件。然后,Kotlin 编译器只是顺序解析所有文件,并收集所有既不是来自类内部也不是单例对象的函数和属性。文件名在这里不起作用。

  • 如果在不同的包中有几个这样的文件(由文件顶部的package声明定义),名称冲突不会引起问题。属性和函数可以使用相同的名称。然而,您应该避免这种情况,以保持代码的可读性。

  • 可以向这样的文件中添加类、接口和单例对象。您可以从声明的地方开始使用这样的结构单元,直到文件结束。

此外,可以使用通配符符号import the.package.name.*从特定包中的所有文件导入属性和函数。这对于避免冗长的import列表非常方便。

练习 1

你有一个实用的单例对象

package com.example.util

object Util {
    fun add10(a:Int) = a + 10
    fun add100(a:Int) = a + 100
}

和一个客户

package com.example

import com.example.util.Util

class A(q:Int) {
    val x10:Int = Util.add10(q)
    val x100:Int = Util.add100(q)
}

你能想出一种方法来重写Util.kt文件以不使用object { }声明吗?客户端代码会是什么样子?

导入函数和属性

单例对象的函数和属性可以通过如下的 import 语句导入

import package.of.the.object.ObjectName.propertyName
import package.of.the.object.ObjectName.functionName

在文件的顶部,在package声明之后,与其他用于导入类和单例对象的import语句一起。这样就可以直接使用函数或属性,只需使用它的名字,而不需要加上ObjectName

注意

没有通配符可以导入单例对象的所有属性和函数。你必须把它们放入各自的行中。

练习 2

假设Math.log()计算一个数的对数,并且Math驻留在包java.lang中,重写

package com.example
class A {
  fun calc(a:Double) = Math.log(a)
}

使得不再需要Math.

数据类别

只包含属性而不包含或包含很少函数的类很可能是*数据类,*其目的是在几个属性周围提供一个括号。因此,它们作为一种容器,聚集了一系列的属性。想象一个Person类,它为一个人收集姓名、生日、出生地、SSN 等等。Kotlin 对此类有一个特殊的符号:Prepend data

data class ClassName([constructor])

这看起来与普通的类没有太大的不同,但是与它们相反,前置data会导致以下结果:

  • 该类根据属性自动获得一个特别定制的toString()函数;你不用自己写。

  • 该类仅基于属性自动获得合理的equals()hashCode()函数。我们稍后会谈到对象平等;现在,您需要知道的是:只有当两个数据类实例属于同一个数据类,并且它们的所有属性都需要相等时,这两个数据类实例的相等检查关系a == b才会产生true

如果您需要一个函数来返回结构化或复合数据,那么数据类就派上了用场。在其他语言中,你必须经常使用成熟的类、数组或列表来实现这个目的,这使得这个任务看起来有点笨拙。在 Kotlin 中,你可以简洁地写出,例如:

data class Point(val x:Double, val y:Double)

fun movePoint(pt:Point, dx:Double, dy:Double):Point =
    Point(pt.x + dx, pt.y + dy)

// somewhere in a function ...
val pt = Point(0.0, 1.0)
val pt2 = movePoint(pt, 0.5, 0.5)

你可以看到,在顶部的单个data class行,我们可以让函数movePoint()返回一个结构化的数据。

练习 3

使用数据类

data class Point2D(val x:Double, val y:Double)
data class Point3D(val x:Double, val y:Double, val z:Double)

眼下,以下哪一项是正确的(==代表等于)?

  1. Point2D(0, 1) == Point2D(1, 0)

  2. Point2D(1, 0) == Point3D(1, 0, 0)

  3. Point2D(1, 0).x == Point3D(1, 0, 0).x

  4. Point2D(1, 0) == Point2D(1.0, 0)

  5. Point2D(1, 0) == Point2D(1, 0)

描述为什么或为什么不。

练习

NumberGuess游戏的哪些类算是数据类?执行转换。

列举

枚举类型基本上是一种非数值数据类型,其值来自给定的集合。这里基本上意味着默认情况下类型由整数处理,但是在基本的使用场景中,你不必担心这个。术语 set 是在数学意义上使用的,这意味着值必须是唯一的,并且没有排序顺序。

Kotlin 中的枚举是类的一种特殊形式:

enum class EnumerationName {
    VALUE1, VALUE2, VALUE3, ...
}

其中,EnumerationName可以使用任何 camelCase 名称,VALUEx是字符集A-Z0-9_中以字母或_开头的任何字符串。

注意

对于这些值,技术上有更多的字符可用,但是按照惯例,您应该使用这里显示的字符组合。

要声明您编写的枚举类型,就像声明任何其他类一样,

val e1: EnumerationName = ...
var e2: EnumerationName = ...

在赋值的右边你用EnumerationName。追加任何枚举值。例如,将水果作为值的枚举声明为 and,并与以下结果一起使用:

enum class Fruit {
    BANANA, APPLE, PINEAPPLE, GRAPE
}

val f1 = Fruit.BANANA
val f2 = Fruit.BANANA
val f3 = Fruit.APPLE
var fx:Fruit? = null

// you can check for equality:
val b1:Boolean = f1 == f2  // -> true
val b2:Boolean = f1 == f3  // -> false

// you can reassign vars:
fx = Fruit.APPLE
fx = Fruit.BANANA

// toString() gives the textual value name
val s = fx.toString() // -> "BANANA"

注意==相当于等号。这是一个布尔表达式,我们还没有正式介绍。如果愿意,您可以自己定义枚举值的内部数据类型:只需向enum类添加一个主构造函数,并将其用于值:

enum class Fruit(val fruitName:String) {
    BANANA("banana"),
    APPLE("apple"),
    PINEAPPLE("pineapple"),
    GRAPE("grape")
}

然后,您可以使用您引入的属性名称来获取这个自定义内部值:

val f1 = Fruit.BANANA
var internalVal = f1.fruitName // -> "banana"

枚举类的一个有趣的内置函数是动态查找函数valueOf():如果您需要从字符串中动态获取一个值,请编写

val f1 = Fruit.valueOf("BANANA")
//     <- same as Fruit.BANANA

使用

EnumerationName.values()

获取枚举的所有值(例如,对于循环)。枚举值本身也有两个内置属性:

  • 使用enumVal.name以字符串形式获取值的名称。

  • 使用enumVal.ordinal获取枚举值列表中值的索引。

练习 5

NumberGuess游戏应用的GameUser类中添加一个Gender枚举。允许值MFX。在默认值为XGameUser构造函数参数中添加相应的构造函数参数gender

自定义属性访问器

我们知道一个var属性基本上是通过书写来声明的

var propertyName:PropertyType = [initial_value]

我们还知道,为了得到var,我们写object.propertyName,为了设置它,我们写object.propertyName =...

在 Kotlin 中,当您获取或设置属性时,可以改变正在发生的事情。为了适应获取过程,您可以这样写:

var propertyName:PropertyType = [initial_value]
    get() = [getting_expression]

[getting_expression]里面,你可以写你喜欢的东西,包括访问函数和其他属性。对于更复杂的情况,你也可以提供一个身体,如

var propertyName:PropertyType = [initial_value]
    get() {
        ...
        return [expression]
    }

改为更改适用于您编写的propertyName = ...的设置过程

var propertyName:PropertyType = [initial_value]
    set(value) { ... }

set主体中,你可以访问对象的所有函数和所有其他属性。此外,您可以使用特殊的field标识符来引用与属性对应的数据。

当然,你可以双管齐下;也就是说,调整获取和设置过程:

var propertyName:PropertyType = [initial_value]
    get() = [getting_expression]
    set(value) { ... }

您可以微调属性的 getters 和 setters 的可见性。只管写

[modifier] var propertyName:PropertyType = ...
    private get
    private set

或任何其他可见性修饰符。但是,要使 getter 成为私有的,属性本身也必须声明为私有的。相反,将 setter 设置为公共属性的私有是一个有效的选择。

有趣的是,可以定义在类或单例对象中没有相应数据的属性。如果您同时定义了属性的 setter 和 getter,并且既没有指定初始值也没有在 setter 代码中使用field,则不会为该属性生成任何数据字段。

练习 6

你能猜到用val代替var属性能做什么吗?

练习 7

编写一个与toString()功能相同的str属性(因此可以编写obj.str而不是obj.toString())。

练习 8

回忆一下NumberGuess游戏 app:

data class GameUser(var firstName:String,
             var lastName:String,
             var userName:String,
             var registrationNumber:Int,
             var gender:Gender = Gender.X,
             var birthday:String = "",
             var userRank:Double = 0.0) {
    enum class Gender{F,M,X}

    var fullName:String
    var initials:String
    init {
      fullName = firstName + " " + lastName
      initials = firstName.toUpperCase() +
                 lastName.toUpperCase()
    }
}

我们遇到的问题是,随着后来的firstName更改,fullName会被破坏。

val u = GameUser("John", "Smith", "jsmith", 123)
u.firstName = "Linda"
val x = u.fullName // -> "John Smith" WRONG!

找到避免这种腐败状态的方法。提示:之后不再需要一个init{ }块。相应地更新您的代码。

Kotlin 扩展

在 Kotlin 中,可以“动态地”向类添加扩展。我们需要将其动态地放在引号中,因为在执行之前,必须在代码中定义这种扩展的用法。在 Kotlin 中,不可能在运行时决定是否要使用哪些扩展,如果要使用的话。计算机语言设计者通常将这样的特性称为静态特性。

这就是我们所说的扩展:如果我们可以给任何类添加函数和自定义属性,那不是很好吗?这可能非常有用,例如,如果我们想要向其他人提供的类和函数添加额外的功能。我们知道我们可以使用继承来达到这个目的,但是根据具体情况,这可能是不可能的,或者实现可能会感觉笨拙。

警告

扩展机制非常强大。小心不要过度使用它。如果不花时间研究扩展定义,您可以使用没有人理解的扩展编写非常优雅的代码。

扩展功能

假设我们希望在内置的String类中有一个hasLength(l:Int): Boolean函数。你可能认为这就是继承的用途。然而,扩展String类是不可能的,因为通过设计来扩展String是被禁止的,所以我们不能为此使用继承。不过,Kotlin 扩展机制在这里帮助了我们。我们可以写作

package the.ext.pckg

fun String.hasLength(len:Int) = this.length == len

在某个包the.ext.pckg内的某个文件fileName.kt(文件名在这里不起作用,所以随便用)。记住==检查平等。

我们现在可以在任何类或单例对象中使用扩展函数

import the.ext.pckg.*

// anywhere inside a function ...
val hasLen10:Boolean = someString.hasLength(10)

同样的过程也适用于任何其他类,包括您自己的类和伴随对象。对于后一种情况,编写fun SomeClass.Companion.ext() { }来定义一个新的扩展函数ext。这里的Companion是一个文字标识符,用于寻址伴随对象。

注意

如果扩展函数与已存在的函数具有相同的名称和函数签名(参数集),则后者优先。

扩展属性

类似的过程也适用于属性。假设您想给String添加一个l属性,它与.length()做同样的事情,并计算字符串长度。您可以通过如下结构来实现:

package the.ext.pckg

val String.l get() = this.length

注意,我们不能使用val String.l = this.length,因为出于技术原因,扩展属性不允许实际创建真正的数据字段。因此初始化是不可能的,因为事实上没有什么可以初始化。至于 getters 我们想怎么写就怎么写,可以直接参考.length。现在可以写了

import the.ext.pckg.*

// anywhere inside a function ...
val len1 = someString.length
val len2 = someString.l // this is the same

具有可空接收器的扩展

注意

接收者指的是被扩展的类或单例对象。

可以捕捉扩展的null值。如果你在前面加上一个问号

fun SomeClass?.newFunction(...) { ... }

你可以检查this == null体内是否还有适当的反应,在这种情况下做正确的事情。即使instancenull,你也可以写instance.newFunction(...),然后进入扩展函数。

封装扩展

如果您想在特定的类、单例对象或伴随对象中封装扩展,可以编写如下代码:

class SomeClass {
    fun SomeOtherClass.meth() {
        ...
    }
}

这里SomeOtherClass接收扩展函数,但是该函数只能从SomeClass内部使用。对于String类的hasLength()扩展,封装版本如下所示

class SomeClass {
    fun String.hasLength(len:Int) = this.length == len
    fun function() {
        ...

        // we can use hasLength() here
        val len10:Boolean = someString.hasLength(10)
        ...
    }
}

class SomeClass2 {
    // we can't use String.hasLength() here
}

类似的过程允许我们封装扩展属性。这些属性的符号如下

class SomeClass {
    val SomeOtherClass.prop get() = ...
}

字符串长度的String.l扩展的封装版本如下

class SomeClass {
    val String.l get() = this.length
    fun function() {
        ...
        // we can use .l here
        val len = someString.l
        ...
    }
}

封装扩展的明显优势是我们不必导入扩展文件。如果我们想要定义可用于许多类的扩展,非封装的变体将是更好的选择。

尾部递归函数

递归函数调用自己。对于某些算法,这种情况偶尔会发生。例如,阶乘函数n! = n(n(n...21可以实现为

fun factorial(n:Int):Int {
    return if(n==1) n else n * factorial(n-1)
}

注意,if()表达式返回在else之前或之后的部分,这取决于参数的计算结果是true还是false(我们将在本书后面讨论分支)。

为了让应用正常运行,运行时引擎需要跟踪函数调用,所以在内部,对factorial()的调用看起来会像factorial( factorial( factorial (...) ) )一样,如果递归深度不太高,这不是问题。但是,如果它真的很高,我们就会遇到内存使用和性能方面的问题。然而,如果递归发生在这样一个函数的最后一条语句中,它可以被转换成一个尾递归函数,这样在内部就不会发生系统资源的过度使用。

要将一个函数转换成尾部递归函数,只需在tailrec前面加上fun,如

tailrec fun factorial(n:Int) {
    return if(n==1) n else n * factorial(n-1)
}

中缀运算符

中缀运算符用于由表示的运算

operand1    OPERATOR    operand2

我们知道很多这样的中缀运算符:想想乘法(3 * 4),加法(3 + 4),等等。在 Kotlin 中,许多这样的中缀运算符是预定义的,但是也可以定义自己的中缀运算符。为此,请编写

infix operator
fun SomeClass1.oper(param:SomeClass2) = ...

其中oper是操作符的名称(使用您自己的名称),...使用this(SomeClass1的实例)和param执行任何计算。然后你可以写

[expression1] oper [expression2]

其中[expression1]的类型为SomeClass1[expression2]的类型为SomeClass2。对于更复杂的计算,您也可以像往常一样使用函数体:

infix operator

fun SomeClass1.oper(param:SomeClass2):ResultType {
    ...
    return [result_expression]
}

例如,为了允许一个字符串使用新操作符TIMES重复 n 次,我们编写

infix operator fun String.TIMES(i:Int) =
    (1..i).map { this }.joinToString("")

(第二行是功能性构造;稍后我们将讨论功能设计。)我们可以接着写

val s = "abc" TIMES 3 // -> "abcabcabc"

如果我们考虑到 Kotlin 有标准操作符的文本对应物,我们可以更巧妙地做到这一点。例如,*,的文本表示是times,所以我们可以写

operator fun String.times(i:Int) =
     (1..i).map { this }.joinToString("")

这样我们就可以使用星号进行相同的操作:

val s = "abc" * 3 // -> "abcabcabc"

这里可以省略infix,因为 Kotlin 知道*属于中缀操作。

使用标准运算符来定义自定义计算称为运算符重载。在下一节中,我们将使用所有标准操作符的列表和文本表示来了解更多信息。

运算符重载

运算符采用一个或两个表达式,并使用以下符号生成一个输出:

[OPER] expression
[expression] [OPER] [expression]

处理一个表达式被称为一元操作,该操作符相应地被称为一元操作符。同样,处理两个表达式给了我们二元操作和二元操作符

从数学中我们知道很多运算符,比如-a,a + b,a * b,a / b 等等。当然,Kotlin 为其数据类型内置了许多这样的操作符,所以7 + 35 * 4等做了预期的事情。我们将在本书后面详细讨论操作符表达式,但现在我们想关注一下操作符重载,Kotlin 的这一功能允许你使用标准操作符为自己的类定义自己的操作符。

比方说,你有一个Point类指定空间中的一个点( x,y ),还有一个Vector类指定两点之间的直接连接。从我们已经学到的,我们知道我们可以通过

data class Point(val x:Double, val y:Double)
data class Vector(val dx:Double, val dy:Double)

现在从数学上我们知道,从点 P 1 到点 P 2 的向量可以写成表达式\overrightarrow{v}={P}_2-{P}_1。计算结果是 dx = * p * 2。x p1。 xdy = * p * 2。y p1。 y 。如果我们可以只写v=p2-p1 来执行那个操作,不是很好吗,就像

val p1 = Point(1.0, 1.0)
val p2 = Point(4.0, -2.0)
val v:Vector = p2 - p1

使用运算符重载,我们可以做到这一点。这很简单:首先,我们需要——操作符的文本表示,恰好是负的。其次我们写

data class Point(val x:Double, val y:Double) {
  operator fun minus(p2:Point) =
       Vector(p2.x-this.x, p2.y-this.y)
}

就是这样。val v:Vector = p2 - p1现在可以工作了,所以每当编译器看到两个Point实例之间有一个-时,它就会计算组合它们的向量。

对于一元运算符,过程是相同的,但是不需要在运算符函数中指定参数。例如,如果你想让-Vector(1.0, 2.0))工作,给定反向向量,你只需加上

operator fun unaryMinus() = Vector(-this.dx, -this.dy)

Vector班。

你可以对 Kotlin 认识的所有操作员做同样的事情。所有这些的文字表示如表 4-1 所示。

表 4-1

经营者

|

标志

|

Arity

|

本文的

|

标准含义

| | --- | --- | --- | --- | | + | U | 一元加号 | 再现数据(如+3)。 | | − | U | 一元减操作 | 对数据求反(例如,7)。 | | ! | U | 不 | 逻辑否定数据(如!true == false)。 | | ++ | U | 股份有限公司 | 递增数据(如var  a  =  6; a++; // -> a == 7)。操作员不得更改调用它的对象!增量值的分配在幕后进行。 | | −− | U | 十二月 | 递减数据(例如var a = 6; a-; // -> a == 5)。操作员不得更改调用它的对象!递减值的分配在幕后进行。 | | + | B | 加 | 添加两个值。 | | − | B | 负的 | 减去两个值。 | | *本文件迟交 | B | 倍 | 将两个值相乘。 | | / | B | 差异 | 将两个值相除。 | | % | B | 雷姆 | 除法的余数(例如,5 % 3 = 2)。 | | .. | B | 范围到 | 创建一个范围(例如 2..5 -> 2, 3, 4, 5) | | 在!在 | B | 包含 | 检查右侧是否包含在左侧中。 | | [ ] | B+ | 获取/设置 | 索引访问。如果在像q[5] = ...这样的赋值的左边,set()函数与指定要设置的值的最后一个参数一起使用。get()set()函数允许多个参数,这些参数对应于[]中几个逗号分隔的索引;比如q[i]q.get(i)q[i,j]q.get(i, j)q[i,j] = 7q.set(i, j, 7) | | ( ) | B+ | 引起 | 祈祷。允许多个参数,这些参数对应于()中几个逗号分隔的参数;比如q(a)q.invoke(a)q(a, b)q.invoke(a, b)。 | | + = | B | plusAssign | 与plus()相同,但是将结果分配给调用操作符的实例。 | | —= | B | 减法赋值 | 与minus()相同,但是将结果分配给调用操作符的实例。 | | *= | B | 时间分配 | 与times()相同,但是将结果分配给调用操作符的实例。 | | / = | B | 二次分配 | 与div()相同,但是将结果分配给调用操作符的实例。 | | % = | B | 再分配 | 与rem()相同,但是将结果分配给调用操作符的实例。 | | == | B | 等于 | 检查是否相等。! =代表不相等,对应equals()返回false。 | | <``>``<=``>= | B | 比较 | 比较两个值。根据参数是小于、等于还是大于函数所应用的值,函数compareTo()应该返回1, 0, +1。 |

注意

因为在操作符函数体或表达式中你可以计算你想要的,你可以让操作符做奇怪的事情。请记住,当使用操作符时,您的类用户期望某个特定的行为,所以合理地使用您在那里计算的内容。

顺便说一下,如果您喜欢在扩展文件中重载操作符,就不需要什么魔法了。对于前面的点和向量的例子,只需写operator fun TheClass.operator_name = ...,如下所示

operator fun Point.minus(p2:Point) =
    Vector(p2.x-this.x, p2.y-this.y)

不要忘记导入扩展文件,就像对任何其他扩展一样。

练习 9

Vector类中添加-+操作符。如果v2是操作函数参数,计算包括增加或减少dxdy成员:Vector(this.dx + v2.dx, this.dy + v2.dy)Vector(this.dx - v2.dx, this.dy - v2.dy)

授权

我们通过class TheClass : SomeInterface {了解到遗传...}TheClass实现接口仅以抽象方式声明的功能。实现代码在TheClass中输入被覆盖的函数。委托类似于继承;它以同样的方式开始:class TheClass : SomeInterface...。不同之处在于实现代码所在的位置:对于委托,假设手边有一个已经实现了接口的对象,而TheClass主要是将工作委托给这个对象。使用我们已知的结构,这可以写成:

interface TheInterface {
    fun someMethod(i:Int):Int
    ...more functions
}

class Implementor0 : SomeInterface {
    override fun someMethod(i:Int):Int = i*2
    ...implement the other functions
}

class Implementor : TheInterface {
    val delegate = Implementor0()
    override fun someMethod(i:Int):Int = delegate(i)
    ...do the same for the other functions
}

Implementor类中的方法someMethod()委托给了delegate,但这也可能会增加一些额外的工作,如

override fun someMethod(i:Int):Int = delegate(i-1) + 1

Kotlin 对委托基本模式有一个简明的符号。你只需要写

class Implementor : TheInterface by Implementor0()
// or
val impl0 = Implementor0()
class Implementor : TheInterface by impl0

然后,Kotlin 编译器通过将工作转发给委托来自动实现所有接口方法。您仍然可以通过覆盖它来更改任何函数:

class Implementor : TheInterface by Implementor0() {
    override fun someMethod(i:Int):Int = i * 42
}

如果您明确需要委托对象,则必须将其添加到构造函数中,如

val b = Implementor0()
class Implementor(val b:TheInterface) :
        TheInterface by b {
    override
    fun someMethod(i:Int):Int = b.someMethod(i-1) + 1
}
val instance = Implementor(b)

五、表达式:对数据的操作

我们已经用过几次表达了。每当你需要给一个变量赋值,需要函数调用参数,或者需要给某种语言结构赋值时,你就需要一个表达式。表达式也会出现在你意想不到的地方,如果我们不需要,它们可以被忽略。

表达式示例

表达式可以细分为不同的类型:数字表达式、布尔表达式、字符串和字符表达式、作用于位和字节的表达式,以及一些未分类的表达式。在我们开始详细解释它们之前,这里有一些例子:

4 * 5         // multiplication
3 + 7         // addition
61         // subtraction
"a" + "b"     // concatenation
( 1 + 2 )     // grouping
-5            // negation
a && b        // boolean a AND b
"Hello"       // constant (String)
78            // another constant (Int)
3.14          // another constant (Double)
'A'           // another constant (Char)
arr[43]       // index access
funct(...)    // function invocation
Clazz()       // instantiation
Obj           // singleton instance access
q.a           // dereferencing
q.f()         // another dereferencing
if(){ }       // language construct
when(){ }     // another language construct

表达式的普遍性

与许多其他计算机语言不同,在 Kotlin 中,几乎所有东西都是表达式。例如,看看函数调用funct()。你可能会认为一个没有像在fun funct() { ... }中那样声明返回值的函数不是一个表达式,因为它似乎不能被赋值给一个变量。试试看,写一写

fun a() {
}

val aa = a()

令人惊讶的是,编译器没有将这段代码标记为错误。事实上,这样的函数确实会返回值;它是Unit类的实例,被称为Unit。你不能用它做任何有趣的事情,但它是一个值,它使一个函数不显式返回任何东西,而是隐式返回一些东西。

在本章的其余部分,我们将介绍不同的表达式类型以及它们之间的转换。

数字表达式

数字表达式是由文字、属性和子表达式等元素构建的结构,可能由运算符组合在一起并产生一个数字。涉及加、减、乘、除的一组众所周知的运算符通常被称为算法。在计算中,这组标准运算符通常会增加一个递增和递减运算符++和*—*,以及一个整数除法余数运算符%。对于 Kotlin 内部可用于数值表达式的可能元素的完整列表,见表 5-1 。

表 5-1

数字表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | 37.5 | | 变量 | 一处房产 | val a = 7; val b = a + 3 | | 函数( ) | 函数的值,如果它返回一个数字 | fun a() = 7; val b = 3 + a() | | [ ] | 访问数组或数字列表中的元素 | arr[0]``list[7] | | ( ) | 替换为内部表达式的结果 | 7 * ( a + b ) | | + | 如果用在表达式前面,则复制数据 | val a = +3``val a = +7.4 | | - | 如果用在表达式前面,则对数据求反 | val a = -(7+2) | | ++ | 可以用在一个var的前面或者后面;如果用在它的前面,则计算为var + 1的当前值;如果在它后面使用,则计算为var的当前值;作为副作用,增加了var | var a = 7``val b = 7 + ++a``val c = 7 + a++ | | -- | 可以用在一个var的前面或者后面;如果用在它的前面,则计算为var - 1 的当前值;如果在它后面使用,则计算为var的当前值;作为副作用,减少了var | var a = 7``val b = 7 + --a``val c = 7 + a-- | | + | 将两个值相加 | 7 + 6 | | - | 减去两个值 | 7 – 6 | | * | 将两个值相乘 | 7 * 6 | | / | 将两个值相除;如果在两个非浮点值之间,则返回一个非浮点值;否则返回一个Double或一个Float | 7 / 6(给出 1)7.0 / 6.0(给出1:16667) | | % | 两个整数值相除的余数 | 5 % 3(给出2) | | subexpr | 用作子表达式的任何表达式,返回一个数字 | 在5 + a / 7中,a/7可以被认为是一个子表达式 |

如果在一个表达式中混合不同类型的数字,具有较大取值范围的数字将被用作返回值的类型,因此用一个Long除以一个Int将得到一个Long:

val l1:Long = 234567890L
val i1:Int = 37
val x = l1 / i1 // -> is a Long

同样,如果您在一个表达式中混合了普通精度的Float元素和双精度的Double元素,Double将获胜:

val f1:Float = 2.45f
val d1:Double = 37.6
val x = f1 / d1 // -> is a Double

将整数与浮点数元素混合会产生浮点数:

val i1:Int = 33
val d1:Double = 37.6
val x = i1 * d1 // -> is a Double

如果我们需要组合三个值(或子表达式)并在一行中有两个操作符,如

  • expr``1expr``2expr**

**问题是先评估哪个操作符。这称为运算符优先级,其 Kotlin 规则如表 5-2 所示。

表 5-2

算术运算符的优先级

|

优先

|

经营者

|

例子

| | --- | --- | --- | | one | ++ --作为后缀 | a++ | | Two | -(在一个表达式前面)+(在一个表达式前面)++ --作为前缀 | –(3 + 4)``--a | | three | * / % | 7 * a | | four | + - | 7 – a |

您总是可以使用圆括号( ... )来指定任何运算符的求值顺序。就像在数学中使用的一样,在使用括号内的解之前,首先计算括号内的值。

练习 1

Math.sqrt(...)表示平方根√,用 Kotlin 代码写下:

\sqrt{\frac{a+\frac{b-x}{2}}{b²-7\cdot x}}

假设a, b,x是现有属性。

布尔表达式

布尔表达式是评估为布尔值truefalse之一的表达式。如果我们需要决定程序的哪些部分参与程序流,我们经常使用布尔表达式。参与布尔表达式的对象和运算符列于表 5-3 。

表 5-3

布尔表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | truefalse | | 变量 | 一处房产 | val a = true; val b = a | | funct() | 函数的值,如果它返回一个布尔值 | fun a() = true; val b = a() | | [ ] | 访问数组或布尔列表中的元素 | arr[0]``list[7] | | ( ) | 替换为内部表达式的结果 | b1 && ( a &#124;&#124; b )(注意:&& = AND,|| = OR) | | && | 和操作;只有当ab都为真时,a && b才为真;注意,如果左边的求值结果为false,那么&&的右边永远不会被求值 | true && true(产量→ true) | | &#124;&#124; | 或者运营;只有当ab中至少有一个为真时,a &#124;&#124; b才为真;注意,如果左边的求值结果为true,那么&#124;&#124;的右边永远不会被求值 | true &#124;&#124; false(产量→ true) | | ! | 对以下布尔表达式求反 | val b = true; val r = !b(yields r为假) | | a == b | 如果ab相等,则产生trueab是任意对象或子表达式;如果布尔或数字子表达式的值相同,则它们相等;如果对象abhashCode()函数返回相同的值,并且a.equals(b)返回true,则它们相等;如果两个字符串都包含相同的字符,则它们相等;如果一个特定数据类的两个实例的所有属性都相等,那么它们就是相等的 | a == 3(如果a的值为 3,则为true)a == "Hello" ( true如果a是字符串“你好”) | | a != b | 不相等,同!( a == b ) | true)true)``false)false) | | a < b | 如果数字a小于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a < 7 (→ true如果a小于 7) | | a > b | 如果数字a大于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a > 3 (→ true如果a大于 3) | | a <= b | 如果数字a小于或等于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a <= 7 (→ true如果a小于或等于 7) | | a >= b | 如果数字a大于或等于数字b,则为真;还评估是否在对象ab上定义了接口Comparable | a >= 3 (→ true如果a大于或等于 3) | | a is b | 如果对象a实现了类或接口b则为真 | true)true) | | a !is b | 同!(a is b) | true)true) | | a === b | 检查引用是否相等;如果对象是same并因此强于==比较,则返回true;通常不经常使用,因为使用==操作符的语义检查在大多数情况下更有意义 | class A``val a = A(); val b = A()``val c = a === b``false)false) |

与上一节中的数值表达式类似,如果使用带有更多运算符的表达式,布尔表达式运算符具有优先权。布尔运算符优先级的 Kotlin 规则如表 5-4 所示。

表 5-4

布尔运算符的优先级

|

优先

|

经营者

|

例子

| | --- | --- | --- | | one | !(在一个表达式前面) | val a = true; val b = !a | | Two | is!is | a in b && c | | three | <<=>=> | a < 7 && b > 5 | | four | ==!= | a == 7 && b != 8 | | five | && | a == 4 && b == 3 | | six | &#124;&#124; | a == 4 &#124;&#124; a == 7 |

对于数值表达式,您可以使用圆括号强制不同的优先顺序:

val b1 = a == 7 && b == 3 || c == 4
val b2 = a == 7 && (b == 3 || c == 4)

如你所见,它们是不同的。在第一行中,&&获胜并首先被计算,因为它比||具有更高的优先级。在第二行中,||获胜,因为它在一个括号内。

字符串和字符表达式

字符串没有太多的表达式元素。但是,您可以连接字符串并执行字符串比较。字符串表达式元素的完整列表见表 5-5 。

表 5-5

字符串表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | "Hello world"或者"""Hello world""" | | 变量 | 一处房产 | val a = "abc"; val b = a | | funct() | 函数的值,如果它返回一个字符串 | fun a() = "abc"; val b = a() | | [ ] | 访问数组或字符串列表中的元素 | arr[0]列表[7] | | str[ ] | 从字符串中提取字符 | "Hello" [1](产量" e ") | | ( ) | 替换为内部表达式的结果 | "ab" + ("cd" + "ef" ) | | + | 串并置 | val a = "Hello " + "world"(产量→ "Hello world") | | a == b | 检查是否相等;如果两个字符串都包含相同的字符,则它们相等 | a == "Hello" ( true如果a是字符串“Hello”) | | a != b | 不相等,同!( a == b ) | true)true) | | a < b | 如果字符串a在字典顺序上小于字符串b则为真 | true)true) | | a > b | 如果字符串a在字典上比字符串b大,则为 True | true)true) | | a <= b | 如果字符串a在字典上小于或等于字符串b则为真 | true)true)``false)false) | | a >= b | 如果字符串a在字典上大于或等于字符串b则为真 | true)true) | | a in b | 如果a是一个Chartrue如果b包含a;如果a是字符串本身,true如果a是字符串b的一部分 | true)true)``true)true) | | a !in b | 同!(a in b) | true)true) |

字符串文字有几种特殊情况。

  • 使用三组双引号的字符串被称为原始字符串。它们可以包含任何内容,包括换行符和反斜杠()等特殊字符。书写"Hello\n world"会产生由换行符分隔的“Hello world”。然而,如果你写"""Hello\n world""",输出将是字面上的“Hello \n world”。一个例外是$;你得写${'$'}才能得到。

  • 在原始和普通(“转义”)字符串中,你都可以使用模板:一个${}被包含在花括号中的任何内容的toString()表示所取代。例如:"The sum of 3 and 4 is ${3+4}"得出字符串“3 和 4 之和是 7”。如果它是一个单一的标识符,比如一个房产的名字,你也可以省略括号,写成$propertyName,比如"And the value of a is $a".

字符具有整数表示,因为它们对应于字符表中的索引。这允许一些算术和比较运算符处理字符。字符表达式元素列表如表 5-6 所示。

表 5-6

字符表达元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | 文字 | 字面意思 | 'A'或者'7' | | 变量 | 一处房产 | val a = 'x'; val b = a | | funct() | 函数的值,如果它返回一个字符 | fun a() = 'x'; val b = a() | | [ ] | 访问数组或字符列表中的元素 | arr[0]``list[7] | | - | 字符表中的距离 | val d = 'c' - 'a'(产量→ 2) | | a == b``a != b``a < b``a > b``a <= b``a >= b | 性格比较;比较字符表中的索引 | 'c' > 'a'(产量→ true) |

比特和字节

字节是更面向硬件的数据存储单位。我们知道有一个Byte类型,它的值在128127之间。一个字节对应于一些硬件存储和处理元素,可以以极快的方式访问和使用。在你的应用中,你只是偶尔使用字节,尤其是在使用一些低级系统功能或寻址连接的硬件元素(如摄像头或扬声器)时。

你知道当你写下125这样的十进制数字系统中的一个数字时,你实际上的意思是51 + 210 + 1100。计算机内部不喜欢十进制计数系统,因为如果他们使用它,例如,78之间的差异不能可靠地用一些技术属性来表示,如两个触点之间的电压。计算机能做得很好的是发现某个东西是否被打开,用密码01来表示。因此,他们在内部使用二进制编码系统。如果我们需要一个125它实际上由二进制数01111101表示,意思是1·1 + 0·2 + 1·223+ 1·24+ 1·25+ 1·26+ 0·27。这个数里面的数字被称为位*,碰巧的是,我们需要 8 位来表示一个字节所有可能的值。***

**因为一个字节是一个数字,你可以用它做所有的事情,我们之前已经讨论过,关于数字表达式。然而,一个字节也是八位的集合,并且有一些特殊的操作可以在位级上进行(见表 5-7 )。注意,ShortIntLong值对应 2、4 和 8 个字节,因此对应 8、16 和 32 位。因此,位级操作不仅可以在字节上执行,还可以在其他整数类型上执行。

表 5-7

位表达式元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | a and b | 位级上的 ANDa的每一位都与b的相应位配对,如果两者都是1,那么结果号中的位也将被设置为1 | 13 and 11(评估为 9: 000011010000101100001001) | | a or b | 位级别上的或;a的每个位与b的相应位配对,如果其中一个或两个都是1,结果号中的位也将被设置1 | 13 or 11(评估为 15: 000011010000101100001111) | | a xor b | 比特级的异或运算;a的每一位都与b的相应位配对,如果其中恰好有一位是1,那么结果号中的位也将被置位1 | 13 xor 11(计算结果为 6: 00001101异或0000101100000110) | | inv a | 将某个数字a的所有位从 0 切换到 1,反之亦然 | inv 13(计算结果为 114: inv 0000110111110010 = 114) | | a shl b | 将所有位从a向左移动b位 | 13 shl 2(评估为 52:0000110100110100 = 52) | | a ushr b | 将所有位从a向右移动b位位置;这个名字是无符号右移的缩写,意味着最左边的位没有得到特殊处理 | 13 shr 2(评估为 3: 0000110100000011 = 3) | | a shr b | 将所有位从a向右移动b位位置;如果最左边的位被设置为1,则每次移位后最左边的位也被设置为1 | -7 shr 2(计算结果为-2: 1111100111111110 = -2) |

注意,有符号右移操作的shr运算符指的是位表示中的负数。这样的负数是这样建立的:确保负数的位和它的算术倒数的位相加在一起正好导致溢出。将*—*3表示为一个字节就产生了11111101,因为这个加上00000011(代表+3)就产生了100000000。一个字节的最后一个九位数导致溢出,最高的第九位丢失,导致零。这最终也给了我们所需的二进制表示形式的+3 +-3 = 0

其他操作员

Kotlin 还有一些我们可以在表达式中使用的操作符。它们不适合区分数字、布尔、字符串和字符以及位表达式,所以我们在表 5-8 中单独列出它们。

表 5-8

其他表达元素

|

标志

|

意义

|

例子

| | --- | --- | --- | | a in b | 检查某个a是否包含在b中,b可能是数组,也可能是集合;一般来说,in操作符适用于任何定义了operator fun contains(other:SomeClass): Boolean函数的对象,甚至是你自己的类 | class B``class A { operator fun``contains(other:B):Boolean``{ ... } }``val b = B()``val a = A()``val contained = b in a | | a !in b | a in b的反面;如果为a的类定义了operator fun contains(other:SomeClass): Boolean也有效 | 参见a in b;添加val notContained = b !in a | | :: | 如果像ClassName::class一样使用,它创建一个对类的引用;如果像ClassName::funNameClassName::propertyName一样使用,它会创建一个对函数或属性的引用 | val c = String::class``val f = String::length | | a .. b | 创建从一个整数(文字、ByteShortIntLongChar ) a到另一个整数b的范围 | 1..100 | | a ?: b | Elvis操作员;如果a不是null,取;否则采取b | var s:String? = ...``var ss = s?:"default"``(如果snull,取“默认”代替) | | a ?. b或者a ?. b() | 安全解引用或安全调用运算符;对于某个对象a,仅当a不是null时,从函数b()调用中检索属性b或结果(可以有参数);否则评估为null本身 | var i:Int? = ...``var ss:String? =``i?.toString() | | a!! | 确保a不是null;否则会引发异常 | var s:String? = ...``var ss = s!!.toString() |

表达式末尾的!! operator不仅检查它不是null,还将其转换为不可空的类型:

val c:Int? = ...    // an int or null
val b = c!!         // b is non-nullable!
// the same: val b:Int = c!!

更好的是,Kotlin 记得我们检查了c不是null,并且对于函数的其余部分,将c视为不可空的属性。

警告

即使!!似乎是一个简化编码的通用工具,你也不应该经常使用它。操作符在某种程度上阻碍了 Kotlin 处理可空性的方式。!!打破了不可为空性,并通过区分可为空和不可为空的类型和表达式隐藏了我们的优势。

练习 2

创建一个允许通过函数add(s:String)连接字符串的类Concatenator。添加另一个函数,这样就可以编写下面的代码来查看连接的字符串是否包含子字符串。

val c = Concatenator()
c.add("Hello")
c.add(" ")
c.add("world")
val contained = "ello" in c

转换策略

如果你有一个val或者var属性或者某种类型的函数参数,问题是如果在赋值中我们提供一个不同类型的表达式会发生什么。如果这种类型不匹配很严重,例如,如果我们需要一个Int号,而提供了一个String,编译器将会失败,我们需要修复它。在其他情况下,例如,如果我们实际上需要一个Long,就提供一个Int,类型之间的简单转换会很好。

Kotlin 通过提供几个可用于手动执行类型转换的函数来帮助我们。在下面的列表中,我们研究了类型不匹配时的选项。

  • 需要一个Int

    • ByteShortIntLong:所有这些都提供了一个到Int()的函数,执行直接转换。

    • Char:有一个toInt()函数,给出字符在字符表中的索引。

    • FloatDouble:提供一个toInt()函数,对于正数,返回给定浮点数下面最接近的Int。对于负数,返回给定浮点数上面最接近的Int。此外,它们还有一个roundToInt()功能,提供向上舍入到下一个整数的功能。

    • String:提供一个toInt()函数,解析给定的字符串,并试图将其转换成一个Int。如果提供的字符串不包含整数,这将失败,因为只允许使用可选符号和 0 到 9 的密码。此外,还有一个toIntOrNull函数处理相同的转换,但不会失败,如果转换不可能,它将返回null。变体toInt(radix:Int)toIntOrNull(radix:Int)使用不同的计数系统(基数)进行转换。例如,对于十六进制基数(使用16作为radix参数),允许使用密码 0 到 9 和字母 A 到 F。

    • Boolean:从布尔值到整数的转换是不可能的。

  • 需要一个LongByteShort

    所有类型ByteShortIntLongCharFloatDoubleString都提供了toLong()toByte()toShort()功能,这些功能遵循与Int目标类型相同的规则,除了适用不同的数字范围。请注意,对于字符串,长文本不允许使用 L 后缀。

  • 需要一个充电器。

    所有整数类型ByteShortIntLong都提供了一个toChar()函数,该函数使用所提供的数字在字符表中执行索引查找。A Char.toChar()原封不动地返回参数。类型FloatDouble提供了一个toChar()函数,该函数首先应用一个toInt(),然后执行字符表查找。字符串不提供到Char的转换,但是您可以使用toCharArray()和索引操作符[]来访问数组元素(例如,"123".toCharArray()[0]给出‘1’)。

  • 需要一个Double或一个Float

    • ByteShortIntLong:这些都提供了toFloat()toDouble()功能,执行明显的转换。

    • Char:字符也有toFloat()toDouble()函数,但是它们返回字符表中转换成浮点数的索引。

    • FloatDouble:这些提供toFloat()toDouble()功能,必要时执行精度转换。

    • String:它有toFloat()toDouble()函数,这些函数试图解析提供的字符串,将其转换成FloatDoubleString可以使用英文格式浮点数表示或科学记数法;比如27.48-3.01.8e4。如果转换不可能,此过程将失败。变量toDoubleOrNull()toFloatOrNull()将尝试相同的转换,但如果出现转换错误,则返回null

    • Boolean:从布尔值到浮点数的转换是不可能的。

  • 需要一个String

    Kotlin 中的任何对象都提供了一个toString()转换,将它翻译成人类可读的表示。对于包含字符的整数,转换很明显;对于浮点数,将选择英语格式;布尔值被翻译成truefalse。类型ByteShortInt,Long也有一个toString(radix:Int)功能,使用提供的编号系统(基数)进行转换。

应用了几个自动转换,所以有可能写val l:Long = 7,这看起来像是自动的IntLong的转换。

注意

根据编码过程中的经验,您可以测试自动转换是否可行,但在大多数情况下,最好显式声明转换。

在运算符起作用的表达式中,适用另一种转换规则。对于任何运营商

  • a**

*其中a是类型ATypeb是类型BType,操作符实现决定了操作结果的类型。一个重要的案例是

[Number]   °  [Number]

其中[Number]选自ByteShortIntLongFloat,Double,运算符为任意数值运算符(+ - / * %)。这里,表达式返回的类型在大多数情况下是具有更高精度的类型。精度排名是Byte<Short<Int<Long<Float<Double。例如:

7 + 10_000_000_000L -> Long
34 + 46.7          -> Double

在 Kotlin 程序中,另一种由操作符引起的转换是

String + [Any]

在这里,字符串和[Any]上的.toString()的结果将发生连接。例如:

"Number is "  +  7.3                ->   "Number is 7.3"
"Number is "  +  7.3.toString()     ->   "Number is 7.3"
"Hell" + 'o'                        ->   "Hello"

```*****

# 六、Kotlin 文件中的注释

计算机语言文件中的注释是不属于计算机语言本身的文本,因此对程序执行没有影响,但提供了程序中使用的元素和结构的文本描述。注释有助于读者理解你的程序。

从技术角度来看,注释很容易生成,并与程序语法本身相区别。

*   以双斜线`//`(不在字符串中)开始到行尾的所有内容都是注释。

*   所有以`/*`开始并以`*/`结束的内容(都不在字符串中)都是注释,不管它跨越了多少行。

乍一看,注释似乎是程序中一个不错的特性,添加或省略注释似乎是每个开发人员的个人决定。不过,评论还有更多的内容。仔细看看这件事,评论是在两个界限之间的范围内处理的:

*   完全不写注释:对于短程序和那些结构非常好、不言自明的程序来说,完全不写注释是一种有效的、尽管有争议的立场。这种方法的优点是显而易见的:您必须编写更少的代码,没有混淆注释和源代码的危险,并且正确地遵循这种方法将会产生高质量的综合代码。但是也有缺点:您可能错误地评估了您的代码是否是自解释的,依赖于注释的工具不提供输出,或者您公司的质量保证指南可能被违反。

*   *冗长的注释* *:* 另一方面,如果你冗长地注释你程序的每一个部分,你将不得不写很多,并且你可能会忽略代码质量,因为程序中模糊或混乱的结构被注释澄清了。

最佳方法介于这些限制之间。作为一个经验法则,你应该为类、接口和单例对象写注释,解释它们的好处,并且你应该在它们中注释公共函数,包括它们的参数描述。

### 注意

我欠你一个坦白。前几章的`NumberGuess`游戏应用在我提供的来源中没有包含任何评论。为了保持列表较小,注释被省略了,这些列表周围的浮动文本充当了读者的替代品。在你读完这一章之后,你可以随意修改这个问题,并给那里的类、接口和单例对象添加适当的注释。

在这一章中,我们将介绍如何将注释添加到 Kotlin 文件中,包括如何使用它们。

## 包注释

我们了解到包与文件相对应,它们的目的和功能有很强的凝聚力。从技术角度来看,每个包也对应于操作系统文件层次结构中的一个目录。

通过适当的注释来描述包是有意义的,我们在 Kotlin 中这样做的方式如下:对于每个包,也就是说在每个文件夹中,创建一个文件`package-info.md`。要在 Android Studio 中实现这一点,你必须在项目浏览器中切换到项目文件视图类型(参见图 6-1 )。单击 Android 旁边的灰色向下小矩形来切换视图类型。然后,您可以右键单击其中一个包,并从快捷菜单中选择“新建➤文件”。输入完整的文件名`package-info.md`。

后缀为`.md`的文件是*降价*文件。Markdown 是一种类似于 HTML 的样式语言,但是有自己简化的语法。我们将很快描述 Markdown,但首先我们必须教会 Android Studio 如何处理 Markdown 文件。为此,双击其中一个新的`package-info.md`文件。工作室在其标准文本编辑器中打开该文件,但它在编辑窗格的顶部显示一条警告消息,如图 6-2 所示。

![img/476388_1_En_6_Fig2_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6d475c99e0c1439b862b7724f0a9fa00~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771248245&x-signature=NOLFejJUKizUuz3w5sC7xZz%2F17o%3D)6-2。

Android Studio 试图打开一个降价文件

![img/476388_1_En_6_Fig1_HTML.jpg](https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/596e8006cdb54ae99da672f0866997e8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771248245&x-signature=XSDe8K%2F5JRIT6KxF%2BE17VIFvJ%2BI%3D)6-1。

项目文件视图

单击安装插件链接。在随后的屏幕上,接受任何许可声明,如果询问,选择使用 JetBrains 的 markdown 支持。

在每个`package-info.md`文件中,让第一行读作

```kt
# Package full.name.of.the.package

在这里,您用每个包的名称来代替full.name.of.the.package。以单个#开头的行实际上代表 1 级标题。

文件的其余部分包含 Markdown 样式的文本。例如,包kotlinforandroid.book.numberguess.random中的package-info.md文件可以读为

# Package kotlinforandroid.book.numberguess.random

This package contains *interfaces* and *classes* for generating random numbers.

In your code you will write something like this:

     val rnd:RandomNumberGenerator = [ one of the 'impl' classes instantiated ]

For example,

     val rnd:RandomNumberGenerator = StdRandom()
     // or
     val rnd:RandomNumberGenerator = RandomRandom()

这些package-info.md文件和我们将在这里讨论的所有其他文档结构可以用来为您的项目生成文档。在这个过程中,*interface*将被翻译成强调的文本,行首有四个空格的段落将应用代码样式格式。反引号(')内的文本将被标记为内联代码。例如,这个特定的降价文件将被翻译成如图 6-3 所示的文档。下一节将描述这些以及所有其他标准的降价语法元素。

img/476388_1_En_6_Fig3_HTML.jpg

图 6-3。

翻译后的降价代码

减价

用于包描述的 Markdown 文件和 Kotlin 代码文件中的内联文档都使用通用的语法来处理样式问题。这些降价语法元素在表 6-1 中描述。

表 6-1。

降价语法

|

风格

|

降价语法

|

暗示

| | --- | --- | --- | | 标题级别 1 | # Heading | package-info.md文件不得包含一个以上的一级标题。您可以在标题行的末尾添加一个#。 | | 标题级别 2–6 | ## Heading``### Heading… | #的数量决定了等级。您可以通过在标题末尾追加相同数量的#来提高可读性。 | | 无序列表 | - Item1``- Item2… | 您也可以使用+或作为项目指示器。 | | 有序列表 | 1\. Item1``2\. Item2… | 连续编号将被自动确保,所以你可以写任何数字(写总是“1”或者别的什么)。 | | 强调 | *some text*或者_some text_ | 如果文本中需要星号()或下划线(),请写\*\_. | | 强烈强调 | **some text**或者_ _some text_ _ | 如果文本中需要星号(*)或下划线(),请写\*\_. | | 批量报价 | > some text | 您可以通过在行首使用更多>字符来提高级别。块引号可以包含其他降价元素。 | | 段落分隔符 | | 某些文本末尾的换行符不会结束一个段落。 | | 环 | 见下文 | — | | 内嵌代码 | 'some text'(反勾号) | 如果你在文本中需要一个反勾号('),写\'. | | 分组码 | t 0t 0… | 这必须用空行包围。⊔是一个空格字符(您也可以使用一个制表符代替)。 | | 规则 | - - -``* * * | 您也可以使用更多的这些字符,并使用空格字符作为分隔符。 | | 逃脱 | 前置一个"\" | 使用它来避免角色做一些特殊的事情,如表中前面所述。合格字符为\ * _ [ ] ( ) # + - . ! ' |

插入链接有几种选择。首先,您可以创建一个内联链接,如下所示:

link text
or
link text

如果文档被转换成 HTML,那么可选的"Title"将进入title属性。例如,一旦鼠标悬停在链接上,就会向用户显示title属性(这种行为取决于所使用的浏览器)。以下是这种内联链接的一个示例:

Find the link here:
[Link](http://www.example.com/somePage.html "Page")

引用链接使用引用 ID,这样你就可以在一个文本中多次引用同一个链接。语法是

[link text][link ID]

其中link ID可以包含字母、空格、数字和标点符号,但不区分大小写。文本中的其他地方需要提供链接定义本身,单独占一行:

[link ID]: link-URL
or
[link ID]: link-URL "Title"

对于长 URL 或长标题,可选的"Title"也可以放在下一行。请注意,链接定义不产生任何输出,它们只是使 Markdown 文件中的文本更容易阅读。

作为一个缩写,链接文本可以作为文本和 ID,如果你写

[link text][]

对于这个定义

[link text]: link-URL
or
[link text]: link-URL "Title"

如果你不需要链接文本,只是想告诉网址,你应该把链接转换成自动链接,用尖括号把它们括起来,如< http://www.apress.com >。然后 URL 按原样打印出来,但也可以点击。

作为上述链接的扩展,您可以引用类、属性和方法,就像它们是隐式链接一样:

[com.example.TheClass]
[com.example.TheClass.property]
[com.example.TheClass.method]

对于接口和单例对象,也可以用同样的方法。如果您想提供自己的链接文本,请这样写:

[link text][com.example.TheClass]
[link text][com.example.TheClass.property]
[link text][com.example.TheClass.method]

如果被记录的元素可能通过它们的简单名称寻址类、接口或单例对象,因为它们已经被导入,那么可以省略包说明符,你可以直接写[TheClass][TheClass.property][TheClass.method].

班级评论

我们知道多行注释可以写成/* ... */。作为一个小小的修改,为了记录代码元素,惯例是在左边的注释括号中添加另一个星号(*):/** ... */,此外,注释中的每一行都应该以星号开头,如下所示:

/**
 *The comment ...
 * ...
 */

这仍然是一个碰巧以星号开始的多行注释,但是知道如何从代码中提取文档的工具认为这是需要处理的事情。你仍然可以随意使用普通的多行注释/* ... */,但是文档工具会忽略它们。

类注释就写在class ...声明的前面,这样一个改编的多行注释/** ... */。如前所述,类描述注释的内容是 Markdown 代码。

此类文档的第一段应该提供一个简短的摘要,因为工具可能会使用它来列出清单。除了标准降价元素之外,您还可以在文档中添加如下元素:

  • @param <name> description:描述类的类型参数<name>。类类型参数将在本书的后面描述。你也可以写@param[name] description

  • @constructor description:描述类的主构造函数。

  • @property <name> description:描述主构造函数的一个参数。

  • @sample <specifier>:插入指定功能的代码。

  • @see <specifier>:添加一个链接到指定的标识符(类、接口、单例对象、属性或方法)。

  • @author description:添加创作信息。

  • @since description:添加关于文档化元素已经存在多长时间的信息(版本信息等)。).

  • @suppress:从文档中排除类、接口或单例对象。

NumberGuessMainActivity类的文档示例

游戏是这样的:

/**
 * The main activity class of the NumberGuess game app.
 * Extends from the
 * [android.support.v7.app.AppCompatActivity]
 * class and is thus compatible with earlier
 * Android versions.
 *
 * The app shows a GUI with the following buttons:
 * - **Start**: Starts the game
 * - **Do Guess**: Used for guessing a number
 *
 * Once started, the game secretly determines a random
 * number the user has to guess. The user performs
 * guesses and gets told if the guessed number is too
 * low, too high, or a hit.
*
 * Once hit, the game is over.
*
 * @see Constants
*
 * @author Peter Späth
 * @since 1.0
 */
class MainActivity : AppCompatActivity() {
    ...
}

相应的输出,一旦被文档工具转换,将如图 6-4 所示。

img/476388_1_En_6_Fig4_HTML.jpg

图 6-4。

数字猜测活动的文档

函数和属性注释

对于函数和属性,您基本上可以像对待类一样进行操作。只要在任何你想注释的函数或者属性前面加上/** ... */就可以了。对于类文档,您可以用任意数量的空格和星号开始每一行。再次使用降价代码。例如:

...
class SomeClass {
    /**
    * This describes property prop
    * ...
    */
    val prop:Int = 7

    /**
     * This describes function func
     * ...
     */
    fun func() {
        ...
     }
}

至于类、接口和单例对象,这类文档的第一段应该提供一个简短的摘要,因为工具可能会用它来列出清单。

对于属性,您可以使用几个附加元素:

  • @sample <specifier>:插入指定功能的代码。

  • @see <specifier>:添加一个链接到指定的标识符(类、接口、单例对象、属性或方法)。

  • @author description:添加创作信息。

  • @since description:添加关于文档化元素已经存在多长时间的信息(版本信息等)。).

  • @suppress:从文件中排除遗产。

函数文档片段还应该描述函数的参数和返回值。具体来说,这里是函数的所有文档元素。

  • @param <name> description:描述一个函数参数。

  • @return description:描述函数返回的内容。

  • @receiver description:描述扩展功能的接收方。

  • @throws <specifier>:表示函数可能抛出由说明符指定的异常。我们将在本书的后面讨论异常。

  • @exception <specifier>:同@throws

  • @sample <specifier>:插入指定功能的代码。

  • @see <specifier>:添加一个链接到指定的标识符(类、接口、单例对象、属性或方法)。

  • @author description:添加创作信息。

  • @since description:添加关于文档化元素已经存在多长时间的信息(版本信息等)。).

  • @suppress:从文件中排除遗产。

练习 1

NumberGuess游戏 app 的所有包、类、公共函数添加评论。

生成您自己的 API 文档

当一个程序的所有元素都被恰当地文档化后,我们现在需要找到一种方法来提取文档,以便创建,例如,一个相互链接的 HTML 文档的集合。生成的文档应该描述所有的类、接口和单例对象,以及所有的公共方法和属性。因为这些元素足以让客户端软件知道如何与你的程序交互,所以这样的文档通常被称为应用编程接口(API)文档

Dokka 是 Kotlin 可以用来创建这种 API 文档的工具。要安装 Dokka,请打开 Android Studio。在 Gradle Scripts 抽屉里(你可能需要切换回 Android 视图类型),有两个名为build.gradle的文件,一个标记为 Project NumberGuess,一个标记为 Module: app(见图 6-5 )。这两个构建文件负责描述如何构建应用以正确运行。这包括声明需要对您的程序可用的库。

img/476388_1_En_6_Fig5_HTML.jpg

图 6-5。

构建脚本

注意

术语通常指其他人构建的程序,你的应用使用其中的部分来执行某些任务。您经常会将库添加到您的项目中,这样您就可以从其他人提供给公众的工作中受益。

打开项目build.gradle,在buildscript {下添加下面一行:

ext.dokka_version = '0.9.17'

在同一个文件中,在dependencies块内还添加(一行):

classpath "org.jetbrains.dokka:
       dokka-android-gradle-plugin:${dokka_version}"

这确保了 Dokka 库被添加到项目中。

现在打开 Moduĺe build.gradle,在所有其他应用插件行的下面,添加

apply plugin: 'org.jetbrains.dokka-android'

在同一个文件中,在底部添加以下内容:

task myDokka(type: org.jetbrains.dokka.gradle.
   DokkaAndroidTask) {
    outputFormat = 'html'
    outputDirectory = "dokka"
    includes = ['packages.md']
    doFirst {
        // build the packages.md file
        def pckg = new File(projectDir.absolutePath +
            File.separator + "packages.md")
        pckg.text = ""
        def s = ""
        projectDir.eachFileRecurse(
              groovy.io.FileType.FILES) { f ->
            if(f.name == 'package-info.md') {
                s += "\n" + f.text
            }
        }
        pckg.text = s
    }
}

这将配置 Dokka 并增加一个准备步骤。

注意

默认情况下,Dokka 不知道如何处理我们的package-info.md文件。相反,它期望一个单独的文件packages.md。准备步骤收集所有的package- info.md文件并建立一个packages.md文件。顺便说一下,这个小脚本是用 Groovy 编写的,这是 Gradle build 系统所依赖的语言。

现在实际执行文档生成,打开窗口最右边的 Gradle 选项卡,然后导航到 NumberGuess: ➤ NumberGuess ➤任务➤文档。双击 myDokka(参见图 6-6 )。

img/476388_1_En_6_Fig6_HTML.jpg

图 6-6。

Dokka 构建任务

现在,您会发现 API 文档是文件夹dokka中相互链接的 HTML 文件的集合(切换到 Project Files 视图类型,在 Android Studio 中查看)。

七、结构性构造

从计算机语言一开始,程序流的条件分支就是程序代码必须能够表达的最基本的东西之一。这种分支发生在函数内部,因此它在类和单例对象内部强加了某种子结构。在这一章中,我们将介绍这样的分支结构,以及帮助我们编写相应代码的辅助类。

如果和何时

在现实生活中,许多行为都是基于决策的。如果满足某些条件,动作 A 发生;否则,会发生动作 B。对于任何编程语言,我们都需要类似的东西,而创建这种程序流分支的最基本方法是古老的 if–else if–else 结构。你检查某个条件,如果满足,if 分支被执行。如果不是,可以选择检查另一个 else if 条件,如果满足这个条件,就执行相应的分支。在可能更多的 else if 子句之后,如果 if 和 else if 检查都没有产生true,则执行最后一个 else 块。

当然,在 Kotlin 中,我们有这样一个 if-else if-else 程序结构,它是这样的

if( [condition] ) {
   [statements1]
} else if( [condition2] ) {
   [statements2]
} else if( [condition3] ) {
   [statements3]
... more "else ifs"
} else {
   [statementsElse]
}

其中所有 else if 和 else 子句都是可选的,并且每个条件的计算结果都必须是布尔值。如何计算这个值取决于你:它可以是一个常数,一个变量,或一个复杂的表达式。作为一个例子,考虑检查某个变量v是否等于某个特定的常数,如果是,调用某个函数abc1()。如果没有,调用函数abc2()代替。代码如下:

if( v == 7 ) {
   abc1()
} else {
   abc2()
}

如果块只包含一条语句,可以省略花括号甚至换行符,所以

if( v == 7) abc1() else abc2()

一行是有效代码。

作为一种特性,类似于 Kotlin 中的大多数其他构造,这样的条件构造可以有一个值,因此可以在表达式中使用。为此,所有语句块的最后一行必须计算相应的数据。

val x = if( [condition] ) {
   [statements1]
   [value1]
} else if( [condition2] ) {
   [statements2]
   [value2]
} else if( [condition3] ) {
   [statements3]
... more "else ifs"
} else {
   [statementsElse]
   [valueElse]
}

这一次 else 子句不是可选的;否则,如果没有 else 值,则完整构造的结果是未定义的。不用说,块末尾的所有值都必须具有相同的期望类型,这个结构才能工作。

类似于非表达式变量,如果块中没有语句,可以省略括号和换行符,因此这是一个有效的语句:

val x = if( a > 3 ) 27 else 28

带有大量 else if 子句的大型条件分支结构相当笨拙。这就是为什么有另一个更简洁的结构,其内容如下:

when( [expression] ) {
   val1 -> { ... }
   val2 -> { ... }
   ...
   else -> { ... }
}

[expression]->前面给出值时,分支{}被执行。这个也能评估出一个值:

val x = when( [expression] ) {
   val1 -> { ... }
   val2 -> { ... }
   ...
   else -> { ... }
}

其中每个{}中的最后一个元素将被用作在相应的检查匹配时返回的值。

为了避免代码块的重复,您还可以定义评估组,如

when( [expression] ) {
   val1      -> { ... }
   val2,val3 -> { ... }
   ...
   else -> { ... }
}

这也适用于价值产出型。

对于->左侧的值,您可以使用任意表达式,包括函数调用:

val x = when( [expression] ) {
   calc(val1) + 7 -> { ... }
   val2,val3      -> { ... }
   ...
   else           -> { ... }
}

此外,我们可以使用一个特殊的in操作符或者它的反操作符!in来进行包含检查:

val l = listOf(...)
val x = when( [expression] ) {
   in l         -> { ... }
   in 27..53    -> { ... }
   !in 100..110 -> { ... }
   ...
   else         -> { ... }
}

这也适用于数组。27..53100..110定义了*范围,*表示它们代表了给定的极限值和之间的所有值。我们将在下一节更详细地讨论范围。

另一个方便的检查是一个特殊的is操作符,它执行类型检查:

val q:Any = ... // any type
val x = when(q) {
   is Int       -> { ... }
   is String    -> { ... }
   ...
   else         -> { ... }
}

还有一个is的否定变体:不出意外,读起来是!is

同样,对于单行代码块,可以省略括号,如下所示:

val q = ... // some Int
val x = when( q ){ 1 -> "Jean" 2 -> "Sam" else -> "" }

如果您需要来自内部when()[expression]用于内部程序块的评估,可以捕获它:

val x = when(val q = [some value]) {
   1 -> q * 3
   2 -> q * 4
   ...
   else -> 0
}

其中捕获变量仅在when块内有效。

范围

范围经常用于循环需要。我们将在下一节讨论循环,所以请将这一节视为准备步骤。范围由两个界限值和两个界限值之间的插值方式定义。

在 Kotlin 中,有三种类型的范围用于IntLongChar类型。使用构造函数,可以按如下方式构建它们:

val r1 = IntRange(1, 1000)
val r2 = LongRange(1, 10_000_000_000)
val r3 = CharRange('A', 'I')

此外,为了达到同样的目的,您可以使用范围运算符..,如下所示:

val r1 = 1..1000
val r2 = 1L..10_000_000_000L
val r3 = 'A'..'I'

最后,一些 Kotlin 标准库函数返回范围或作用于范围。任何整数类型(即ByteShortIntLongChar))都有一个rangeTo()函数来创建一个范围。因此,也可以通过编写7.rangeTo(77)来构建7..77

范围还有一个step属性,它定义了如何在范围边界之间插值。默认情况下,步长为+1,但您可以按如下方式进行调整:

1..1000 step 5
(1..1000 step 5).reversed()

其中最后一行的reversed()交换边界并否定该步骤。请注意,根据语言设计,不允许显式指定负步长。然而,允许使用downTo操作符:

1000 downTo 1 step 5

如果使用firstlast属性,范围表示第一个和最后一个值:

(1..1000 step 5).first          // -> 1
(1..1000 step 5).last           // -> 996
(1000 downTo 1 step 5).first    // -> 1000
(1000 downTo 1 step 5).last     // -> 5

For 和 While 循环

循环对应于反复迭代多次的程序部分。这种循环的一种可能是for循环,如下所示:

for( i in [loop data] ) {
    // do something with i
}

其中[loop data]是一个范围、一个集合、一个数组或任何其他具有函数iterator()的对象,返回一个具有next():EhasNext():Boolean函数的对象(E是循环变量类型)。在后一种情况下,所有三个功能iterator()next()hasNext()必须标有operator

for循环类似的还有whiledo .. while循环,它们会继续循环,直到某个条件产生false:

while( [condition] ) {
    // do something
}

do {

    // do something
} while( [condition] )

其中,在第一种情况下,在最开始时检查条件,在第二种情况下,在任何迭代(包括第一次)结束时检查条件。

forwhile循环都可以通过在内部程序流中使用break来优先退出。同样,在循环中的任何地方使用continue语句都会强制进行下一次迭代,忽略continue后面的任何内容:

while( [condition] ) {
    ...
    break // -> exit loop
    ...
    continue // -> next iteration
    ...
}

或者类似地用于fordo .. while循环。

注意

Forwhile循环现在被认为是非常老派的。在集合上使用forEach()可以更好地控制循环准备动作,比如转换和过滤,所以比起forwhile,更喜欢使用forEach()。在后面的章节中,我们会谈到很多关于集合和集合数据的迭代。

范围函数

当涉及到代码的表现力时,Kotlin 的几个标准库函数非常强大。其中的五个applyletalsorunwith被称为作用域函数,因为它们在函数内部打开了一个新的作用域,从而改善了程序流的结构。让我们看看他们做了什么,以及他们如何帮助我们写出更好的代码。

注意

顺便说一句,如果你需要一个助记符来记住它们,读读“让我们也用 APPLY 运行”

应用功能

让我们看看这些作用域函数中的第一个,apply。你可以把它挂在任何物体上,比如

object.apply {
    ...
}

这看起来并不太冒险,但是神奇的是在apply的花括号内的对象实例发生了什么:它被传输到this。另外,apply自动返回对象实例。因此,如果我们写this.somePropertysomeProperty,或this.someFunction()别名someFunction(),它指的是apply前面的object,而不是周围的上下文。这是什么意思?好吧,想想这个:

class A { var x:Int, var y:Int }
val instance = A()
instance.x = 4
instance.y = 5
instance.y *= instance.x

如果我们现在将.apply{}写在已初始化的对象后面,我们可以使用this来访问实例并获得

class A { var x:Int, var y:Int }
val instance = A().apply{
    this.x = 4
    this.y = 5
    this.y *= this.x
}

其可以进一步缩短,因为this.可以省略:

class A { var x:Int, var y:Int }
val instance = A().apply{
    x = 4
    y = 5
    y *= x
}

注意

因为propertyNamefunctionName()针对的是this实例,所以我们也可以说this代表了这种简单属性和函数访问的接收者。没有作用域函数,this指的是周围的类实例或单例对象。随着thisapply{ ... }中被重新定义,.apply前面的实例成为新的接收者。

如果在apply{}构造中使用的属性或函数标识符在 receiver 对象中不存在,则使用周围的上下文:

var q = 37
class A { var x:Int, var y:Int }
val instance = A().apply {
    x = 4
    y = 5
    q = 44 // does not exist in A, so the q from
           // outside gets used
}

apply{}被操作的对象与同一对象接收的花括号内的this作用域函数和属性之间的这种强耦合,使得apply{}构造成为在对象实例化后立即准备对象的极好候选:

val x = SomeClass().apply {
    // do things with the SomeClass instance
    // while assigning it to x
}

来自周围上下文(类或单例对象)的this不会丢失。如果您在apply{}中需要它,您可以通过添加一个限定符@Class来获得它,如

class A {
    fun goA() { ... }
    ...
    val x = SomeClass().apply {
        this.x = ...    // -> SomeClass.x
        x = ...         // -> SomeClass.x
        this@A.goA()    // -> A.goA()
        ...
    }
}

字母功能

let作用域函数经常被用来将一个对象转换成一个不同的对象。它的完整概要是这样的:

object.let { o ->
    [statements] // do s.th. with 'o'
    [value]
}

最后一行必须包含let{}应该返回的表达式。let{}构造有一个函数作为参数,如果你像这里这样写它,并使用一个匿名的 lambda 函数和o作为参数,这个参数函数获得对象本身作为参数。您也可以省略o ->,在这种情况下,会自动使用一个特殊变量it:

object.let {
    [statements] // do s.th. with 'it'
    [value]
}

注意

在花括号内写没有x ->let { },看起来好像{ }是一个功能块。这是一个句法上的巧合;实际上,它是一个匿名的 lambda 函数,以自动变量it为参数。

以其他函数为参数的函数称为高阶函数。我们将在第十二章中讲述高阶函数。

举个简单的例子,我们取一个字符串,用let{}给它附加一个换行符"\n":

val s = "Hello World"
val s2 = s.let { it + "\n" }
// or    s.let { string -> string + "\n" }

with 函数

with作用域函数是apply{}的兄弟。不同之处在于,它只是获取要转换为接收方的对象或值作为参数:

val o = ... // some value
with(o){
    // o is now "this"
    ...
}

with函数经常用于避免重复编写要操作的对象,如

with(object){ f1(37)
    f1(12)
    fx("Hello")
}

代替

object.f1(37)
object.f1(12)
object.fx("Hello")

“也”函数

also作用域函数与apply{}函数相关,但不重新定义this。相反,它将also前面的对象或值作为参数提供给 lambda 函数参数:

object.also { obj ->
    // 'obj' is object
    ...
}

或者

object.also {
    // 'it' is object
    ...
}

您将also{ }用于横切关注点,这意味着您不改变对象(这就是apply{}的目的),但是执行与当前程序流无关的动作。执行缓存、日志记录、身份验证或在某个注册表对象中注册对象都是合适的例子。

运行功能

run作用域函数类似于apply{}函数。但是,它不返回 receiver 对象,而是返回最后一条语句的值:

val s = "Hello"
val x = s.run {
    // 'this' is 's'
    ...
    [value]
}
// x now has [value]

你可以把run{}看做一个通用的“用一个物体做点什么”的括号。不过,一个突出的用例是,只在对象不为空时才处理它。代替

var v:String? = ...
...
if(v != null) {
    ...
}

你可以写作

var v:String? = ...
...
v?.run {
    ...
}

记住,只有当前面的对象不是null时,?.才会访问一个属性或调用一个函数。在某些情况下,更简洁的后一种变体可能更具可读性。

条件执行

允许我们将条件分支编写为实例函数的结构如下所示:

someInstance.takeIf { [boolean_expression] }?.run {
    // do something
}

在布尔表达式中,您可以使用it来引用someInstance。如果布尔表达式的计算结果为true,则takeIf()函数返回接收者(这里是someInstance);否则返回null。这适用于任何对象。