分享点kotlin的小技巧

前言

其实吧,现在网上关于 kotlin 的奇技淫巧真的不少,我写这个的主要目的还是对近来使用 kotlin 的一个小小总结。

就 kotlin 的特性而言,有时候与其说是 “写” kotlin,不如说是在想怎么 “玩” kotlin。这也是我对 kotlin 钟情的一个原因,因为有时候某个写法真的会有一种令人耳目一新的感觉。而写出这些代码的前提就是要对 kotlin 的基础有住够的了解。虽然我用 kotlin 写过一两个 Android 项目,但是现在回过头来看,当时也不过只是了解点简单语法而已,写出来的也充满了浓浓的 Java 味,完全没有 kotlin 的感觉。

不过近两个多月,我一直坚持每天在 leetcode 上用 kotlin 来写算法,频率是每天至少一题(我的 solution 代码放在了我的 Algorithms 上)。这期间也参考了一些 kotlin 的写法,发现并体会到了很多 kotlin 的妙处,当然也包括算法知识,所以下面我会分享一些我学到与我总结出的一些技巧

两数交换

这并不是很难的事情,那么为什么要讲呢?

初学者写两数交换,估计是跟 java 差不多的

1
2
3
4
5
6
7
var a = 1
var b = 2

// swap
val temp = a
a = b
b = temp

当然你也有可能采用不使用第三个变量的方式

1
2
3
4
// swap
a = a xor b
b = a xor b
a = a xor b

这些都能实现两数交换,我在刚开始接触 kotlin 的时候就在想有没有一种写法能够像 Python 的 a, b = b, a 或者 JavaScript 的 [a, b] = [b, a] ,用一行来解决

我想到了一种写法,就是使用 Pair。Pair 需要指定两个泛型,并且接收两个参数,kotlin 有类型推到的特性,所以你直接传入参数的话,就可以不用指定泛型

1
2
// swap
Pair(a, b).let{ a = it.second; b = it.first }

first 默认是你传入的第一个参数,second 默认是你传入的第二个参数

这样看起来还是有点别扭,我们还可以继续优化,使用 to 来构造 Pair

1
2
// swap
(a to b).let{ a = it.second; b = it.first }

不过,let 函数内的 it 要写两次,也是有点烦的,那么我们就可以考虑把 let 换成 with,这样代码就可以更短一点

1
2
// swap
with(a to b) { a = second; b = first }

或者使用 run

1
2
// swap
(a to b).run { a = second; b = first }

虽然没有 Python 和 JavaScript 的简洁,但我觉得这种写法更具有 kotlin 的味道

构造一个 Map

有时候我们需要借助 map 键值对的特性来完成某些任务

举个个简单的例子,统计字符串内每个字母出现的次数

比如有个字符串 val str = "jaskldjfhakljashdfiwjhialsjhlkblzjmxd"

其中一个思路就是使用 map 来存储,key 为字母,value 为字母出现次数

1
2
3
4
5
6
7
val str = "jaskldjfhakljashdfiwjhialsjhlkblzjmxd"
val store = HashMap<Char, Int>()
str.forEach {
if (!store.containsKey(it)) store[it] = 0
store[it] = store[it]!! + 1
}
store.forEach(::println)

因为 kotlin 的空安全特性,每次取出时必须保证这个键值对是存在的,所以在更新键值对时都需要判空一下,这样写似乎也没什么复杂的,不过我再一次浏览别人代码的时候,发现别人是这么处理键值对的判空与更新的

1
store[it] = (store[it] ?: 0) + 1

借助 kotlin 的判空特性直接省略了 if 语句,并且避免了使用多余的!!,瞬间就觉得清爽了很多,因为代码缩短为了一行,所以这段码看起来会更加的紧凑

1
2
3
4
val str = "jaskldjfhakljashdfiwjhialsjhlkblzjmxd"
val store = HashMap<Char, Int>()
str.forEach { store[it] = (store[it] ?: 0) + 1 }
store.forEach(::println)

当然这里也可以使用 Map 的扩展函数 getOrElse,用起来也很简洁

1
str.forEach { store[it] = store.getOrElse(it, { 0 }) + 1 }

利用好集合和数组

我刚开始学或者刚开始用 kotlin 的数组的时候,就吐槽 kotlin 的数组很难用,因为它写起来真的很复杂,甚至初学不熟悉语法的话,有时候想写个数组都有点困难,那么先来捋捋这个数组的写法

创建空数组

创建个长度为 10 的空一维整型数组

1
val ints = IntArray(10)

对应的字符数组就是 CharArray(10) ,float 数组就对应 FloatArray(10) 其他类型以此类推

创建空数组时初始化

诸如 XxxArray 这种数组的构造方式由两个,如果你只传入一个长度,就是一个指定长度的空数组。第二种构造方式就是,在传入长度的同时,传入一个 lambda 表达式用来初始化,如果你想初始化一个长度为 10 的全 1 数组

1
val ints = IntArray(10) { 1 }

或者有一定规则排列的数,比如从 0 开始的连续偶数

1
val ints = IntArray(10) { 2 * it }

初学的时候就困惑在这个 lambda 表达式,如果是规则排列的还好,那如果不是呢?哪又得如何构建呢?Kotlin 官方当然会想到这一点,所以接下来就是第三个数组写法

创建一个含有初始元素的数组

1
val ints = intArrayOf(1,4,5,2,6,7,9,8,0,3)

同样的,其它类型就把 int 更换掉就可以了,不过因为 kotlin 具有类型推到的特性,所以下面的写法也是可以的

1
val ints = arrayOf(1,4,5,2,6,7,9,8,0,3)

所以大致总结下

  • 创建指定长 Xxx 数组用 XxxArray(len)
  • 创建指定长并具有一定排列规则的 Xxx 数组时用 XxxArray(len) { initial }
  • 创建含初始元素的数组用 xxxArrayOf(*args)

集合的初始化

kotlin 有个 MutableList,其中 Mutable 译为可变的,与此相对的数组就可以理解为长度不可变的

所以 MutableList 就没办法直接指定一个长度为 N 的空集合而不初始化

1
val list = MutableList(10) { it * 2 }

可以把 MutableList 当作原始数组的一个衍生。但是如果我就是想创建个空集,怎么做呢?在我还不知道方法前,我一直用的都是 kotlin 的 ArrayList() ,不过后来发现可以把 MutableList 写成如下形式

1
val list = mutableListOf<Int>()

不用指定长度与初始化,指定了元素类型即可,因为长度可变,所以直接使用 list.add(arg) 来添加新元素

数组和集合中的扩展

上面讲了些只是铺垫,下面来分享点想讲的

因为数组和集合中很多操作是差不多的,所以这里可以统一讲

判空

如果部门要对一个集合或数组判空,我们一般是这么做的 !arr.isEmpty()

不过,在 kotlin 中就可以这样 arr.isNotEmpty()

获取最后一个元素

在 kotlin 中可以通过 arr.lastIndex 来获取最后一个下标,这个值是 arr.size - 1

同时,如果要拿到最后一个元素的话,也不需要先获取长度再取元素,直接 arr.last(),不过如果 arr 是空的话会抛出异常,所以调用之前还需要判空一下

更优雅的添加或删除元素

在集合中想添加一个元素,我们会使用 add 方法,删除可以使用 remove 方法

不过,在 kotlin 中有一种语法糖,我们可以直接使用 +- 来添加或删除元素

1
2
3
4
5
6
7
8
9
10
11
// 创建一个 0 到 9 的集合
val list = MutableList(10) { it }

list += 10 // 添加元素 10
list -= 0 // 删除元素 0 (从前往后看,删除第一个,没有该元素 list 则没变化)

list += arrayOf(11, 12, 13) // 把数组内的元素添加进 list 中
list -= arrayOf(13, 14, 15) // list 含有元素 13,所以 13 会删除掉

list.forEach { print("$it ") }
println() // 输出 1 2 3 4 5 6 7 8 9 10 11 12

数组可以使用 + 但不能使用 -,并且数组的长度是固定的,所以它不会改变,因此会创建一个新的数组

1
2
3
4
5
6
7
8
// 创建一个 0 到 9 的数组,需定义为 var
var arr = IntArray(10) { it }

arr += 10 // 将原数组的内容添加一个元素 10 并返回一个新数组
arr += intArrayOf(11, 12) // 将原始数组的内容与新数组的内容拼接生成新数组,因此这里数组类型需相同

arr.forEach { print("$it ") }
println() // 输出 0 1 2 3 4 5 6 7 8 9 10 11 12

其他

  • 在数组和集合中可以直接使用 [] 来获取元素 (这个应该是大家都知道的)
  • 使用 arr.sort() 直接对数组或集合内容进行排序,内部调用的是 java 中的 Arrays 的 sort 方法
  • 使用 arr.max() 获取数组或集合内的最大值
  • 使用 arr.min() 获取数组或集合内的最小值
  • 使用 arr.getOrElse(index: Int, defaultValue: (Int) -> Int) 来获取一个元素可以防止出现越界等极端情况
  • 使用 arr.indices 获取一个范围为 [0, arr.size-1] 的整型数组,可以用来遍历
  • … …

善用扩展方法和高阶函数

kotlin 为我们提供了丰富的扩展方法和高阶函数,合理的使用能使得代码更加的紧凑与简洁。(当然还得考虑可读性,不过在开发项目时为了保证可读性我觉得还是保持 KISS 原则的好,怎么简单怎么来,不过如果只是你一个人的话,那当然是怎么开心怎么来咯 😀)

Author: Inno Fang
Link: http://innofang.github.io/2018/02/23/%E5%88%86%E4%BA%AB%E7%82%B9kotlin%E7%9A%84%E5%B0%8F%E6%8A%80%E5%B7%A7/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-ND 4.0 unless stating additionally.