总结 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.14
。eq
和neq
都是真值断言,它们会显示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是一种混合的面向对象和函数式语言:它支持面向对象编程和函数式编程范式。
对象包含val
和var
来存储数据(这些被称为属性),并使用在类内部定义的函数执行操作,称为成员函数(当不会引起歧义时,我们简称为“函数”)。类定义了属性和成员函数,用于实质上是一个新的、用户定义的数据类型。当你创建一个类的val
或var
时,它被称为创建一个对象或创建一个实例。
一种特别有用的对象类型是容器,也称为集合。容器是包含其他对象的对象。在本书中,我们经常使用List
,因为它是最通用的序列。在这里,我们对一个包含Double
的List
执行了几个操作。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
关键字、类名和一个可选的主体组成。主体包含属性定义(val
和var
)和函数定义。
这个例子定义了一个没有主体的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
}
这些成员函数与我们在类外定义的顶层函数几乎一样,只是它们属于类并且可以不加限定地访问类的其他成员,例如current
和scale
。成员函数还可以在同一个类中无需限定地调用其他成员函数。
- [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
类持有一个包含三个List
的List
,每个List
包含三个Cell
,形成一个矩阵。
- [1]
Cell
中的entry
属性是一个var
,因此它可以被修改。初始化中的单引号产生一个Char
类型,因此对entry
的所有赋值都必须是Char
类型的。 - [2]
setValue()
函数检查Cell
是否可用,并且你传递了正确的字符。它返回一个String
结果来表示成功或失败。 - [3]
play()
函数检查x
和y
参数是否在范围内,然后通过索引进入矩阵,依赖于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
中的参数id
和years
只在构造函数体中可用。构造函数体由除了函数定义之外的代码行组成;在这个例子中,是name
和age
的定义。
通常情况下,你希望构造函数的参数在类的其他部分中也可用,而不需要像name
和age
那样显式定义新的标识符。如果将参数声明为var
或val
,它们将成为属性,并且可以在类的任何地方访问。Snake
和Moose
都使用了这种方法,你可以看到构造函数参数现在可以在它们各自的toString()
函数内部使用了。
使用val
声明的构造函数参数不能被修改,而使用var
声明的参数可以被修改。
每当在期望String
的情况下使用一个对象时,Kotlin会通过调用其toString()
成员函数来生成该对象的String
表示。要定义一个toString()
函数,你必须理解一个新的关键字:override
。这是必要的(Kotlin要求这样做),因为toString()
已经被定义了。override
告诉Kotlin我们确实想用自己的定义替换默认的toString()
。override
的显式性使得这一点对读者清晰可见,并有助于防止错误。
注意Snake
和Moose
中多行参数列表的格式化方式——当你有太多参数无法在一行上容纳时,这是推荐的标准,适用于构造函数和函数。
限制可见性
Kotlin提供了类似于C++或Java等其他语言中的访问修饰符。这些修饰符允许组件创建者决定对客户程序员可见的内容。Kotlin的访问修饰符包括public
、private
、protected
和internal
关键字。protected
会在后面进行解释。
像public
或private
这样的访问修饰符出现在类、函数或属性的定义之前。每个访问修饰符仅控制特定定义的访问权限。
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]
fuel
和warning
都是private
属性,非JetPack
的成员无法使用它们。 - [2]
burn()
是private
的,因此只能在JetPack
内部访问。 - [3]
fly()
和check()
是public
的,可以在任何地方使用。 - [4] 没有访问修饰符意味着
public
可见性。 - [5] 只有相同类的成员才能访问
private
成员。
因为private
定义对于所有人都不可用,所以你通常可以放心更改它,而不用担心客户端程序员的影响。作为库设计者,你通常会尽可能将所有内容保持为private
,只暴露你希望客户端程序员使用的函数和类。为了限制本书中示例清单的大小和复杂性,我们只在特殊情况下使用private
。
任何你确定只是一个辅助函数的函数都可以设置为private
,以确保你不会意外地在其他地方使用它,从而限制了你更改或删除该函数的能力。
将大型程序划分为模块可能是有用的。模块是代码库的逻辑独立部分。internal
定义仅在定义它的模块内部可访问。如何将项目划分为模块取决于构建系统(例如Gradle或Maven),超出了本书的范围。
模块是一个更高级的概念,而包允许更细粒度的结构化。
异常
考虑toDouble()
函数,它将一个String
转换为一个Double
。如果你对一个无法转换为Double
的String
调用它,会发生什么呢?
// 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
解法是二次方程公式:

二次方程公式
该示例找出了抛物线的零点,即抛物线与x轴相交的点。我们为两个限制条件抛出异常:
a
不能为零。- 为了使零点存在,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
引用 second
。second
的只读特性并不阻止通过 first
对 List
进行修改。
// 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)
}
first
和 second
引用的是同一个内存中的对象。我们通过 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
个元素的新 List
。slice()
生成一个包含由其 Range
参数选择的元素的新 List
,而此 Range
可以包含一个步长。
请注意 sorted()
的命名,而不是 sort()
。当您调用 sorted()
时,它会生成一个已排序的 List
,而不会修改原始的 List
。sort()
只适用于 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()
对于numbers
和strings
的定义,我们添加了冒号和类型声明List<Int>
和List<String>
。尖括号表示类型参数,允许我们说:“容器中保存了‘参数’对象”。通常将List<Int>
读作“Int
的List
”。
返回值也可以有类型参数,就像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] 使用
in
或contains()
操作符来测试元素是否属于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找到。