Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

总结 2

本章节总结了第二部分的所有原子,从对象无处不在属性访问器

如果你是一名有经验的程序员,在阅读完总结1之后,可以按顺序阅读本章节的原子。

新手程序员应该阅读本原子,并完成相关的练习作为复习。如果你对其中的任何内容不清楚,请返回并学习相关主题的原子。

这些主题按照经验丰富的程序员的适当顺序列出,这与书中的原子顺序不同。例如,我们首先介绍包和导入,以便在本原子的其余部分中使用我们的最小测试框架。

包和测试

使用 package 关键字可以将任意数量的可重用库组件打包到一个库名称下:

// Summary2/ALibrary.kt
package com.yoururl.libraryname

// Components to reuse ... 可重用组件
fun f() = "result"

你可以将多个组件放在一个文件中,也可以将组件分散在同一包名下的多个文件中。在这个例子中,我们将 f() 定义为唯一的组件。

为了使其独特,包名惯例上以你的域名的反转形式开始。在这个例子中,域名是 yoururl.com

在 Kotlin 中,包名可以独立于其内容所在的目录。而在 Java 中,要求目录结构与完全限定的包名相对应,所以包 com.yoururl.libraryname 应该位于 com/yoururl/libraryname 目录下。对于混合 Kotlin 和 Java 的项目,Kotlin 的样式指南建议使用相同的做法。对于纯 Kotlin 项目,请将 libraryname 目录放在项目目录结构的顶级。

import 语句将一个或多个名称引入当前命名空间:

// Summary2/UseALibrary.kt
import com.yoururl.libraryname.*

fun main() {
  val x = f()
}

libraryname 后面的星号告诉 Kotlin 导入库的所有组件。你也可以逐个选择组件;详细信息请参阅

在本书的其余部分,我们会为定义函数、类等的文件使用 package 语句,以避免与书中的其他文件发生名称冲突。我们通常不会在 仅包含 main() 的文件中放置 package 语句。

本书中一个重要的库是 atomictest,我们简单的测试框架。atomictest附录 A: AtomicTest中定义,尽管它使用了你目前可能不理解的语言特性。

在导入atomictest之后,你可以像使用语言关键字一样使用eq(等于)和neq(不等于):

// Summary2/UsingAtomicTest.kt
import atomictest.*

fun main() {
  val pi = 3.14
  val pie = "A round dessert"
  pi eq 3.14
  pie eq "A round dessert"
  pi neq pie
}
/* 输出:
3.14
A round dessert
3.14
*/

在没有任何点号或括号的情况下使用eq/neq的能力称为中缀表示法。你可以以常规方式调用infix函数:pi.eq(3.14),或者使用中缀表示法:pi eq 3.14eqneq都是真值断言,它们会显示eq/neq语句左侧的结果,以及如果右侧表达式与左侧不等效(或者与neq的情况相等)时的错误消息。这样,你可以在源代码中看到经过验证的结果。

atomictest.trace使用函数调用语法来添加结果,然后可以使用eq来验证:

// Testing/UsingTrace.kt
import atomictest.*

fun main() {
  trace("Hello,")
  trace(47)
  trace("World!")
  trace eq """
    Hello,
    47
    World!
  """
}

你可以有效地用trace()替换println()

Kotlin是一种混合的面向对象和函数式语言:它支持面向对象编程和函数式编程范式。

对象包含valvar来存储数据(这些被称为属性),并使用在类内部定义的函数执行操作,称为成员函数(当不会引起歧义时,我们简称为“函数”)。定义了属性和成员函数,用于实质上是一个新的、用户定义的数据类型。当你创建一个类的valvar时,它被称为创建一个对象创建一个实例

一种特别有用的对象类型是容器,也称为集合。容器是包含其他对象的对象。在本书中,我们经常使用List,因为它是最通用的序列。在这里,我们对一个包含DoubleList执行了几个操作。listOf()根据其参数创建一个新的List

// Summary2/ListCollection.kt
import atomictest.eq

fun main() {
  val lst = listOf(19.2, 88.3, 22.1)
  lst[1] eq 88.3  // 索引
  lst.reversed() eq listOf(22.1, 88.3, 19.2)
  lst.sorted() eq listOf(19.2, 22.1, 88.3)
  lst.sum() eq 129.6
}

使用List不需要import语句。

Kotlin使用方括号对序列进行索引。索引是从零开始的。

这个例子还展示了一些可用于List的许多标准库函数:sorted()reversed()sum()。要了解这些函数的详细信息,请参阅在线的Kotlin文档

当调用sorted()reversed()时,lst不会被修改。相反,将创建并返回一个新的List,其中包含所需的结果。这种永不修改原始对象的方法在整个Kotlin库中是一致的,你应该在编写自己的代码时努力遵循这种模式。

创建类

一个类定义由class关键字、类名和一个可选的主体组成。主体包含属性定义(valvar)和函数定义。

这个例子定义了一个没有主体的NoBody类,以及带有val属性的类:

// Summary2/ClassBodies.kt
package summary2

class NoBody

class SomeBody {
  val name = "Janet Doe"
}

class EveryBody {
  val all = listOf(SomeBody(),
    SomeBody(), SomeBody())
}

fun main() {
  val nb = NoBody()
  val sb = SomeBody()
  val eb = EveryBody()
}

要创建一个类的实例,将其名称后面加上括号,以及如果需要的话还可以加上参数。

类主体中的属性可以是任何类型。SomeBody包含一个类型为String的属性,而EveryBody的属性是一个包含SomeBody对象的List

下面是一个带有成员函数的类:

// Summary2/Temperature.kt
package summary2
import atomictest.eq

class Temperature {
  var current = 0.0
  var scale = "f"
  fun setFahrenheit(now: Double) {
    current = now
    scale = "f"
  }
  fun setCelsius(now: Double) {
    current = now
    scale = "c"
  }
  fun getFahrenheit(): Double =
    if (scale == "f")
      current
    else
      current * 9.0 / 5.0 + 32.0
  fun getCelsius(): Double =
    if (scale == "c")
      current
    else
      (current - 32.0) * 5.0 / 9.0
}

fun main() {
  val temp = Temperature()   // [1]
  temp.setFahrenheit(98.6)
  temp.getFahrenheit() eq 98.6
  temp.getCelsius() eq 37.0
  temp.setCelsius(100.0)
  temp.getFahrenheit() eq 212.0
}

这些成员函数与我们在类外定义的顶层函数几乎一样,只是它们属于类并且可以不加限定地访问类的其他成员,例如currentscale。成员函数还可以在同一个类中无需限定地调用其他成员函数。

  • [1] 虽然temp是一个val,但我们稍后修改了Temperature对象。val定义阻止了引用temp被重新赋值为一个新对象,但它不限制对象本身的行为。

以下两个类是井字棋游戏的基础:

// Summary2/TicTacToe.kt
package summary2
import atomictest.eq

class Cell {
  var entry = ' '                   // [1]
  fun setValue(e: Char): String =   // [2]
    if (entry == ' ' &&
      (e == 'X' || e == 'O')) {
      entry = e
      "Successful move"
    } else
      "Invalid move"
}

class Grid {
  val cells = listOf(
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell()),
    listOf(Cell(), Cell(), Cell())
  )
  fun play(e: Char, x: Int, y: Int): String =
    if (x !in 0..2 || y !in 0..2)
      "Invalid move"
    else
      cells[x][y].setValue(e)       // [3]
}

fun main() {
  val grid = Grid()
  grid.play('X', 1, 1) eq "Successful move"
  grid.play('X', 1, 1) eq "Invalid move"
  grid.play('O', 1, 3) eq "Invalid move"
}

Grid类持有一个包含三个ListList,每个List包含三个Cell,形成一个矩阵。

  • [1] Cell中的entry属性是一个var,因此它可以被修改。初始化中的单引号产生一个Char类型,因此对entry的所有赋值都必须是Char类型的。
  • [2] setValue()函数检查Cell是否可用,并且你传递了正确的字符。它返回一个String结果来表示成功或失败。
  • [3] play()函数检查xy参数是否在范围内,然后通过索引进入矩阵,依赖于setValue()执行的测试。

构造函数

构造函数用于创建新对象。你可以通过在类名后面的括号中传递参数列表来向构造函数传递信息。构造函数的调用看起来类似于函数调用,只是名称的首字母大写(遵循 Kotlin 的风格指南)。构造函数返回一个该类的对象。

// Summary2/WildAnimals.kt
package summary2
import atomictest.eq

class Badger(id: String, years: Int) {
  val name = id
  val age = years
  override fun toString(): String {
    return "Badger: $name, age: $age"
  }
}

class Snake(
  var type: String,
  var length: Double
) {
  override fun toString(): String {
    return "Snake: $type, length: $length"
  }
}

class Moose(
  val age: Int,
  val height: Double
) {
  override fun toString(): String {
    return "Moose, age: $age, height: $height"
  }
}

fun main() {
  Badger("Bob", 11) eq "Badger: Bob, age: 11"
  Snake("Garden", 2.4) eq
    "Snake: Garden, length: 2.4"
  Moose(16, 7.2) eq
    "Moose, age: 16, height: 7.2"
}

Badger中的参数idyears只在构造函数体中可用。构造函数体由除了函数定义之外的代码行组成;在这个例子中,是nameage的定义。

通常情况下,你希望构造函数的参数在类的其他部分中也可用,而不需要像nameage那样显式定义新的标识符。如果将参数声明为varval,它们将成为属性,并且可以在类的任何地方访问。SnakeMoose都使用了这种方法,你可以看到构造函数参数现在可以在它们各自的toString()函数内部使用了。

使用val声明的构造函数参数不能被修改,而使用var声明的参数可以被修改。

每当在期望String的情况下使用一个对象时,Kotlin会通过调用其toString()成员函数来生成该对象的String表示。要定义一个toString()函数,你必须理解一个新的关键字:override。这是必要的(Kotlin要求这样做),因为toString()已经被定义了。override告诉Kotlin我们确实想用自己的定义替换默认的toString()override的显式性使得这一点对读者清晰可见,并有助于防止错误。

注意SnakeMoose中多行参数列表的格式化方式——当你有太多参数无法在一行上容纳时,这是推荐的标准,适用于构造函数和函数。

限制可见性

Kotlin提供了类似于C++或Java等其他语言中的访问修饰符。这些修饰符允许组件创建者决定对客户程序员可见的内容。Kotlin的访问修饰符包括publicprivateprotectedinternal关键字。protected会在后面进行解释。

publicprivate这样的访问修饰符出现在类、函数或属性的定义之前。每个访问修饰符仅控制特定定义的访问权限。

public定义对所有人都可见,尤其是对使用该组件的客户程序员可见。因此,对public定义的任何更改都会影响客户端代码。

如果你不提供修饰符,你的定义会自动成为public。为了在某些情况下保持清晰,程序员有时仍然会冗余地指定public

如果将类、顶层函数或属性定义为private,它只在该文件内可用。

// Summary2/Boxes.kt
package summary2
import atomictest.*

private var count = 0                   // [1]

private class Box(val dimension: Int) { // [2]
  fun volume() =
    dimension * dimension * dimension
  override fun toString() =
    "Box volume: ${volume()}"
}

private fun countBox(box: Box) {        // [3]
  trace("$box")
  count++
}

fun countBoxes() {
  countBox(Box(4))
  countBox(Box(5))
}

fun main() {
  countBoxes()
  trace("$count boxes")
  trace eq """
    Box volume: 64
    Box volume: 125
    2 boxes
  """
}

你只能从Boxes.kt文件的其他函数和类中访问private属性([1])、类([2])和函数([3])。Kotlin阻止你从另一个文件访问private顶层元素。

类成员可以是private的:

// Summary2/JetPack.kt
package summary2
import atomictest.eq

class JetPack(
  private var fuel: Double   // [1]
) {
  private var warning = false
  private fun burn() =       // [2]
    if (fuel - 1 <= 0) {
      fuel = 0.0
      warning = true
    } else
      fuel -= 1
  public fun fly() = burn()  // [3]
  fun check() =              // [4]
    if (warning)             // [5]
      "Warning"
    else
      "OK"
}

fun main() {
  val jetPack = JetPack(3.0)
  while (jetPack.check() != "Warning") {
    jetPack.check() eq "OK"
    jetPack.fly()
  }
  jetPack.check() eq "Warning"
}
  • [1] fuelwarning都是private属性,非JetPack的成员无法使用它们。
  • [2] burn()private的,因此只能在JetPack内部访问。
  • [3] fly()check()public的,可以在任何地方使用。
  • [4] 没有访问修饰符意味着public可见性。
  • [5] 只有相同类的成员才能访问private成员。

因为private定义对于所有人都不可用,所以你通常可以放心更改它,而不用担心客户端程序员的影响。作为库设计者,你通常会尽可能将所有内容保持为private,只暴露你希望客户端程序员使用的函数和类。为了限制本书中示例清单的大小和复杂性,我们只在特殊情况下使用private

任何你确定只是一个辅助函数的函数都可以设置为private,以确保你不会意外地在其他地方使用它,从而限制了你更改或删除该函数的能力。

将大型程序划分为模块可能是有用的。模块是代码库的逻辑独立部分。internal定义仅在定义它的模块内部可访问。如何将项目划分为模块取决于构建系统(例如GradleMaven),超出了本书的范围。

模块是一个更高级的概念,而允许更细粒度的结构化。

异常

考虑toDouble()函数,它将一个String转换为一个Double。如果你对一个无法转换为DoubleString调用它,会发生什么呢?

// Summary2/ToDoubleException.kt

fun main() {
  // val i = "$1.9".toDouble()
}

取消注释main()函数中的那一行会导致异常。在这里,我们将失败的那一行注释掉,这样可以避免停止书籍的构建过程(构建过程会检查每个示例是否按预期编译和运行)。

当抛出异常时,当前的执行路径停止,异常对象从当前上下文中退出。如果异常未被捕获,程序会中止并显示一个包含详细信息的堆栈跟踪

为了避免通过注释和取消注释代码来显示异常,atomictest.capture()函数可以捕获异常并将其与我们的期望进行比较:

// Summary2/AtomicTestCapture.kt
import atomictest.*

fun main() {
  capture {
    "$1.9".toDouble()
  } eq "NumberFormatException: " +
    """For input string: '$1.9'"""
}

capture()函数是专门为本书设计的,它可以让您查看异常并知道输出已经经过了书籍的构建系统检查。

当您的函数无法成功生成预期结果时,另一种策略是返回null。稍后在可空类型中,我们将讨论null如何影响结果表达式的类型。

要抛出异常,使用throw关键字,后面跟随您想要抛出的异常以及它可能需要的任何参数。以下示例中的quadraticZeroes()函数解决了定义抛物线的二次方程

ax2 + bx + c = 0

解法是二次方程公式

quadratic formula

二次方程公式

该示例找出了抛物线的零点,即抛物线与x轴相交的点。我们为两个限制条件抛出异常:

  1. a 不能为零。
  2. 为了使零点存在,b2 - 4ac 不能为负。

如果零点存在,则有两个零点,因此我们创建了Roots类来保存返回值:

// Summary2/Quadratic.kt
package summary2
import kotlin.math.sqrt
import atomictest.*

class Roots(
  val root1: Double,
  val root2: Double
)

fun quadraticZeroes(
  a: Double,
  b: Double,
  c: Double
): Roots {
  if (a == 0.0)
    throw IllegalArgumentException(
      "a is zero")
  val underRadical = b * b - 4 * a * c
  if (underRadical < 0)
    throw IllegalArgumentException(
      "Negative underRadical: $underRadical")
  val squareRoot = sqrt(underRadical)
  val root1 = (-b - squareRoot) / 2 * a
  val root2 = (-b + squareRoot) / 2 * a
  return Roots(root1, root2)
}

fun main() {
  capture {
    quadraticZeroes(0.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "a is zero"
  capture {
    quadraticZeroes(3.0, 4.0, 5.0)
  } eq "IllegalArgumentException: " +
    "Negative underRadical: -44.0"
  val roots = quadraticZeroes(3.0, 8.0, 5.0)
  roots.root1 eq -15.0
  roots.root2 eq -9.0
}

在这里,我们使用了标准异常类IllegalArgumentException。稍后您将学习如何定义自己的异常类型,并使它们针对您的特定情况。您的目标是生成尽可能有用的消息,以便将来更容易支持您的应用程序。

列表

List是Kotlin的基本顺序容器类型。您可以使用listOf()创建一个只读列表,使用mutableListOf()创建一个可变列表:

// Summary2/ReadonlyVsMutableList.kt
import atomictest.*

fun main() {
  val ints = listOf(5, 13, 9)
  // ints.add(11) // 'add()' not available
  for (i in ints) {
    if (i > 10) {
      trace(i)
    }
  }
  val chars = mutableListOf('a', 'b', 'c')
  chars.add('d') // 'add()' available
  chars += 'e'
  trace(chars)
  trace eq """
    13
    [a, b, c, d, e]
  """
}

基本的 List 是只读的,不包含修改函数。因此,修改函数 add()ints 上不起作用。

for 循环与 List 配合使用很好:for(i in ints) 表示 i 获取 ints 中的每个值。

chars 被创建为 MutableList,可以使用 add()remove() 等函数对其进行修改。你也可以使用 +=-= 来添加或删除元素。

只读的 List不可变List 是不同的,后者根本无法被修改。在这里,我们将可变的 List first 赋值给只读的 List 引用 secondsecond 的只读特性并不阻止通过 firstList 进行修改。

// Summary2/MultipleListReferences.kt
import atomictest.eq

fun main() {
  val first = mutableListOf(1)
  val second: List<Int> = first
  second eq listOf(1)
  first += 2
  // second sees the change:
  second eq listOf(1, 2)
}

firstsecond 引用的是同一个内存中的对象。我们通过 first 引用对 List 进行修改,然后在 second 引用中观察到这个变化。

这是一个由三引号段落分割而成的 String 列表。这展示了一些标准库函数的强大之处。请注意这些函数如何可以链式调用:

// Summary2/ListOfStrings.kt
import atomictest.*

fun main() {
  val wocky = """
    Twas brillig, and the slithy toves
      Did gyre and gimble in the wabe:
    All mimsy were the borogoves,
      And the mome raths outgrabe.
  """.trim().split(Regex("\\W+"))
  trace(wocky.take(5))
  trace(wocky.slice(6..12))
  trace(wocky.slice(6..18 step 2))
  trace(wocky.sorted().takeLast(5))
  trace(wocky.sorted().distinct().takeLast(5))
  trace eq """
    [Twas, brillig, and, the, slithy]
    [Did, gyre, and, gimble, in, the, wabe]
    [Did, and, in, wabe, mimsy, the, And]
    [the, the, toves, wabe, were]
    [slithy, the, toves, wabe, were]
  """
}

trim() 生成一个新的 String,去除开头和结尾的空白字符(包括换行符)。split() 根据参数将 String 分割成多个部分。在这种情况下,我们使用一个 Regex 对象,它创建了一个正则表达式,即匹配要分割的部分的模式。\W 是一个特殊的模式,表示“非单词字符”,+ 表示“一个或多个前面的字符”。因此,split() 将在一个或多个非单词字符处进行分割,从而将文本块分割为组成单词的部分。

String 字面值中,\ 位于特殊字符之前,并产生特殊字符,例如换行符 (\n) 或制表符 (\t)。要在生成的字符串中产生一个实际的 \,您需要使用两个反斜杠:"\\"。因此,所有正则表达式都需要额外的 \ 来插入反斜杠,除非您使用三引号的 String"""\W+"""

take(n) 生成一个包含前 n 个元素的新 Listslice() 生成一个包含由其 Range 参数选择的元素的新 List,而此 Range 可以包含一个步长。

请注意 sorted() 的命名,而不是 sort()。当您调用 sorted() 时,它会生成一个已排序的 List,而不会修改原始的 Listsort() 只适用于 MutableList,并且会就地对列表进行排序,即修改原始的 List

正如其名称所示,takeLast(n) 生成一个包含最后 n 个元素的新 List。从输出中可以看到,“the” 被重复了。通过在调用链中添加 distinct() 函数可以消除重复项。

参数化类型

参数化类型允许我们描述复合类型,最常见的是容器类型。特别是,类型参数指定了容器所包含的内容。在这里,我们告诉 Kotlin numbers 包含一个 List 类型的 Int,而 strings 包含一个 List 类型的 String

// Summary2/ExplicitTyping.kt
package summary2
import atomictest.eq

fun main() {
  val numbers: List<Int> = listOf(1, 2, 3)
  val strings: List<String> =
    listOf("one", "two", "three")
  numbers eq "[1, 2, 3]"
  strings eq "[one, two, three]"
  toCharList("seven") eq "[s, e, v, e, n]"
}

fun toCharList(s: String): List<Char> =
  s.toList()

对于numbersstrings的定义,我们添加了冒号和类型声明List<Int>List<String>。尖括号表示类型参数,允许我们说:“容器中保存了‘参数’对象”。通常将List<Int>读作“IntList”。

返回值也可以有类型参数,就像toCharList()函数中所示。你不能只说它返回一个List,Kotlin会报错,因此你必须给出类型参数。

可变参数列表

vararg关键字是可变参数列表的简写形式,允许函数接受任意数量(包括零个)指定类型的参数。vararg会变成一个Array,它类似于一个List

// Summary2/VarArgs.kt
package summary2
import atomictest.*

fun varargs(s: String, vararg ints: Int) {
  for (i in ints) {
    trace("$i")
  }
  trace(s)
}

fun main() {
  varargs("primes", 5, 7, 11, 13, 17, 19, 23)
  trace eq "5 7 11 13 17 19 23 primes"
}

函数定义只能指定一个参数为vararg。列表中的任何参数都可以是vararg,但通常最后一个参数最简单。

你可以在任何接受vararg的地方传递一个元素的Array。要创建一个Array,可以使用arrayOf(),方式与使用listOf()类似。注意,Array始终是可变的。要将一个Array转换为一系列参数(不仅仅是一个类型为Array的单个元素),可以使用展开操作符*

// Summary2/ArraySpread.kt
import summary2.varargs
import atomictest.trace

fun main() {
  val array = intArrayOf(4, 5)      // [1]
  varargs("x", 1, 2, 3, *array, 6)  // [2]
  val list = listOf(9, 10, 11)
  varargs(
    "y", 7, 8, *list.toIntArray())  // [3]
  trace eq "1 2 3 4 5 6 x 7 8 9 10 11 y"
}

如果像上面的例子一样传递原始类型的Array,那么创建Array的函数必须具有特定的类型。如果**[1]使用arrayOf(4, 5)而不是intArrayOf(4, 5)[2]**会产生错误:推断类型为Array<Int>,但期望的是IntArray

展开操作符只适用于数组。如果你有一个List要作为一系列参数传递,首先将其转换为Array,然后应用展开操作符,如**[3]**所示。因为结果是一个原始类型的Array,我们必须使用特定的转换函数toIntArray()

集合

Set是只允许包含唯一值的集合。Set会自动防止重复元素的存在。

// Summary2/ColorSet.kt
package summary2
import atomictest.eq

val colors =
  "Yellow Green Green Blue"
    .split(Regex("""\W+""")).sorted()  // [1]

fun main() {
  colors eq
    listOf("Blue", "Green", "Green", "Yellow")
  val colorSet = colors.toSet()        // [2]
  colorSet eq
    setOf("Yellow", "Green", "Blue")
  (colorSet + colorSet) eq colorSet    // [3]
  val mSet = colorSet.toMutableSet()   // [4]
  mSet -= "Blue"
  mSet += "Red"                        // [5]
  mSet eq
    setOf("Yellow", "Green", "Red")
  // Set membership:
  ("Green" in colorSet) eq true        // [6]
  colorSet.contains("Red") eq false
}
  • [1] 使用之前在ListOfStrings.kt中描述的正则表达式,将字符串进行split()操作。
  • [2]colors复制到只读的Set colorSet中时,其中一个重复的字符串"Green"被移除,因为集合中只允许唯一的元素。
  • [3] 在这里,我们使用+操作符创建并显示一个新的Set。将重复的元素放入Set会自动移除这些重复项。
  • [4] 使用toMutableSet()函数可以从只读的Set创建一个新的MutableSet
  • [5] 对于MutableSet+=-=操作符用于添加和删除元素,与MutableList的操作类似。
  • [6] 使用incontains()操作符来测试元素是否属于Set

所有常规的数学集合操作,如并集、交集、差集等,都可用于Set

映射

Map用于将进行关联,并在给定键时查找对应的值。通过向mapOf()函数提供键值对来创建Map。使用to关键字将每个键与其关联的值分隔开:

// Summary2/ASCIIMap.kt
import atomictest.eq

fun main() {
  val ascii = mapOf(
    "A" to 65,
    "B" to 66,
    "C" to 67,
    "I" to 73,
    "J" to 74,
    "K" to 75
  )
  ascii eq
    "{A=65, B=66, C=67, I=73, J=74, K=75}"
  ascii["B"] eq 66                   // [1]
  ascii.keys eq "[A, B, C, I, J, K]"
  ascii.values eq
    "[65, 66, 67, 73, 74, 75]"
  var kv = ""
  for (entry in ascii) {             // [2]
    kv += "${entry.key}:${entry.value},"
  }
  kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
  kv = ""
  for ((key, value) in ascii)        // [3]
    kv += "$key:$value,"
  kv eq "A:65,B:66,C:67,I:73,J:74,K:75,"
  val mutable = ascii.toMutableMap() // [4]
  mutable.remove("I")
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75}"
  mutable.put("Z", 90)
  mutable eq
    "{A=65, B=66, C=67, J=74, K=75, Z=90}"
  mutable.clear()
  mutable["A"] = 100
  mutable eq "{A=100}"
}
  • [1] 使用键("B")通过[]操作符查找对应的值。你可以使用keys访问所有的键,使用values访问所有的值。访问keys会产生一个Set,因为Map中的所有键必须是唯一的(否则在查找时会存在歧义)。
  • [2] 遍历Map会产生键值对的映射项。
  • [3] 在迭代时可以解构键值对。
  • [4] 使用toMutableMap()可以从只读的Map创建一个MutableMap。现在我们可以对mutable进行修改操作,比如remove()put()clear()。方括号可以将一个新的键值对赋值给mutable。你也可以通过map += key to value的方式添加一对键值对。

属性访问器

访问属性i看起来很直观:

// Summary2/PropertyReadWrite.kt
package summary2
import atomictest.eq

class Holder(var i: Int)

fun main() {
  val holder = Holder(10)
  holder.i eq 10 // Read the 'i' property
  holder.i = 20  // Write to the 'i' property
}

然而,Kotlin调用函数来执行读取和写入操作。这些函数的默认行为是读取和写入存储在i中的数据。通过创建属性访问器,你可以改变读取和写入过程中发生的操作。

用于获取属性值的访问器称为getter。要创建自己的getter,需在属性声明之后立即定义get()函数。用于修改可变属性的访问器称为setter。要创建自己的setter,需在属性声明之后立即定义set()函数。定义getter和setter的顺序并不重要,你可以单独定义其中一个。

下面的示例中的属性访问器模仿了默认实现,同时显示额外的信息,以便你可以看到属性访问器在读取和写入时确实被调用。我们对get()set()函数进行了缩进,以在视觉上将它们与属性关联起来,但实际的关联是因为它们在属性之后立即定义的:

// Summary2/GetterAndSetter.kt
package summary2
import atomictest.*

class GetterAndSetter {
  var i: Int = 0
    get() {
      trace("get()")
      return field
    }
    set(value) {
      trace("set($value)")
      field = value
    }
}

fun main() {
  val gs = GetterAndSetter()
  gs.i = 2
  trace(gs.i)
  trace eq """
    set(2)
    get()
    2
  """
}

在getter和setter内部,使用field关键字间接操作存储的值,该关键字只能在这两个函数内部访问。你也可以创建一个没有field的属性,而是简单地调用getter来产生结果。

如果声明一个private属性,那么两个访问器都将变为private。你可以将setter设置为private,getter设置为public。这意味着你可以在类外读取属性,但只能在类内部更改其值。

练习和解答可在www.AtomicKotlin.com找到。