写在开头: 本人打算开始写一个 Kotlin 系列的教程, 一是使自己记忆和理解的更加深刻, 二是可以分享给同样想学习 Kotlin 的同学系列文章的知识点会以 Kotlin 实战这本书中顺序编写, 在将书中知识点展示出来同时, 我也会添加对应的 Java 代码用于对比学习和更好的理解
Kotlin 教程 (一) 基础
[Kotlin 教程 (三) 类对象和接口]
[Kotlin 教程(四)Lambda]
[Kotlin 教程 (五) 类型系统]
在 Kotlin 中创建集合
上一章我们已经使用 setOf 函数创建一个 set 了同样的, 我们也可以用类似的方法创建一个 list 或者 map:
- val set = setOf(1, 2, 3)
- val list = listOf(1, 2, 3)
- val map = mapOf(1 to "one", 2 to "two")
to 并不是一个特殊的结构, 而是一个普通函数, 在后面会继续探讨它 有没有想过这里创建出来的 setlistmap 到底是什么类型的那? 可以通过. javaClass 属性获取类型, 相当于 Java 中的 getClass() 方法:
- println(set.javaClass)
- println(list.javaClass)
- println(map.javaClass)
- // 输出
- class java.util.LinkedHashSet
- class java.util.Arrays$ArrayList
- class java.util.LinkedHashMap
可以看到都是标准的 Java 集合类, Kotlin 没有自己专门的集合类, 是为了更容易与 Java 代码交互, 当从 Kotlin 中调用 Java 函数的时候, 不用转换它的集合类来匹配 Java 的类, 反之亦然 尽管 Kotlin 的集合类和 Java 的集合类完全一致, 但 Kotlin 还不止于此举个例子, 可以通过以下方法来获取一个列表中最后一个元素, 或者得到一个数字列表的最大值:
- val strings = listOf("first", "second", "fourteenth")
- println(strings.last())
- val numbers = setOf(1, 14, 2)
- println(numbers.max())
- // 输出
- fourteenth
- 14
或许你应该知道 last() 和 max() 在 Java 的集合类中并不存在, 这应该是 Kotlin 自己扩展的方法, 可以你要知道上面我们打印出来的类型明确是 Java 中的集合类, 但在这里调用方法的对象就是这些集合类, 又是怎么做到让一个 Java 中的类调用它本身没有的方法那? 在后面我们讲到扩展函数的时候你就会知道了!
让函数更好调用
现在我们知道了如何创建一个集合, 接下来让我们打印它的内容 Java 的集合都有一个默认的 toString 实现, 但它的何世华的输出是固定的, 而且往往不是你需要的样子:
- val list = listOf(1, 2, 3)
- println(list) // 触发 toString 的调用
- // 输出
- [1, 2, 3]
假设你需要用分号来分隔每一个元素, 然后用括号括起来, 而不是采用默认实现要解决这个问题, Java 项目会使用第三方库, 比如 Guava 和 Apache Commons, 或者是在这个项目中重写打印函数在 Kotlin 中, 它的标准库中有一个专门的函数来处理这种情况 但是这里我们先不借助 Kotlin 的工具, 而是自己写实现函数:
- fun <T> joinToString(
- collection: Collection<T>,
- separator: String,
- prefix: String,
- postfix: String
- ): String {
- val result = StringBuilder(prefix)
- for ((index, element) in collection.withIndex()) {
- if (index> 0) result.append(separator) // 不用再第一个元素前添加分隔符
- result.append(element)
- }
- result.append(postfix)
- return result.toString()
- }
这个函数是泛型, 它可以支持元素为任意类型的集合让我们来验证一下, 这个函数是否可行:
- val list = listOf(1, 2, 3)
- println(joinToString(list, ";", "(", ")"))
- // 输出
- (1;2;3)
看来是可行的, 接下来我们要考虑的是如何修改让这个函数的调用更加简洁呢? 毕竟每次调用都要传入四个参数也是挺麻烦的
命名参数
我们关注的第一个问题就是函数的可读性就以 joinToString 来看:
joinToString(list, "","", "")
你能看得出这些 String 都对应什么参数吗? 可能必须要借助 IDE 工具或者查看函数说明或者函数本身才能知道这些参数的含义 在 Kotlin 中, 可以做的更优雅:
println(joinToString(list, separator = "", prefix ="", postfix = ""))
当你调用一个 Kotlin 定义的函数时, 可以显示得标明一些参数的名称如果在调用一个函数时, 指明了一个参数的名称, 为了避免混淆, 那它之后的所有参数都需要标明名称
ps: 当你在 Kotlin 中调用 Java 定义的函数时, 不能采用命名参数因为把参数名称存到 .class 文件是 Java8 以及更高版本的一个可选功能, 而 Kotlin 需要保持和 Java6 的兼容性
可能到这里你只是觉得命名参数让函数便于理解, 但是调用变得复杂了, 我还得多写参数的名称! 别急, 与下面说的默认参数相结合时, 你就知道命名参数的好了
默认参数值
Java 的另一个普遍存在问题是: 一些类的重载函数实在太多了这些重载大多是为了向后兼容, 方便 API 的使用者, 最终导致的结果是重复 在 Kotlin 中, 可以在声明函数的时候, 指定参数的默认值, 这样可以避免创建重载的函数让我们尝试改进一下前面的 joinToString 函数在大多数情况下, 我们可能只会改变分隔符或者改变前后缀, 所以我们把这些设置为默认值:
- fun <T> joinToString(
- collection: Collection<T>,
- separator: String = ",",
- prefix: String = "",
- postfix: String = ""
- ): String {
- val result = StringBuilder(prefix)
- for ((index, element) in collection.withIndex()) {
- if (index> 0) result.append(separator) // 不用再第一个元素前添加分隔符
- result.append(element)
- }
- result.append(postfix)
- return result.toString()
- }
现在在调用一下这个函数, 可以省略掉有默认值的参数, 效果就像在 Java 中声明的重载函数一样
- println(joinToString(list))
- println(joinToString(list, ";"))
- // 输出
- 1,2,3
- 1;2;3
当你使用常规的调用语法时, 必须按照函数申明中定义的参数顺序来给定参数, 可以省略的只有排在末尾的参数如果使用命名参数, 可以省略中的一些参数, 也可以以你想要的任意 顺序只给定你需要的参数:
- // 打乱了参数顺序, 并且 separator 参数使用了默认值
- println(joinToString(prefix = "{", collection = list, postfix = "}"))
- // 输出
- {1,2,3}
注意, 参数的默认值是被编译到被调用的函数中, 而不是调用的地方如果你改变了参数默认值并重新编译这个函数, 没有给参数重新赋值的调用者, 将会开始使用新的默认值
Java 没有参数默认值的概念, 当你从 Java 中调用 Kotlin 函数的时候, 必须显示得指定所有参数值如果需要从 Java 代码中调用也能更简便, 可以使用 @JvmOverloads 注解函数这个指示编译器生成 Java 的重载函数, 从最后一个开始省略每个函数例如 joinToString 函数, 编译器就会生成如下重载函数: public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) public static final String joinToString(@NotNull Collection collection, @NotNull String separator) public static final String joinToString(@NotNull Collection collection) 因此, 当你项目同时存在 Java 和 Kotlin 时, 对有默认参数值的函数习惯性地加上 @JvmOverloads 注解是个不错的做法
消除静态工具类: 顶层函数和属性
Java 作为一门面对对象的语言, 需要所有的代码都写作类的函数但实际上项目中总有一些函数不属于任何一个类, 最终产生了一些类不包含任何状态或者实例函数, 仅仅是作为一堆静态函数的容器在 JDK 中, 最明显的例子应该就是 Collections 了, 还有你的项目中是不是有很多以 Util 作为后缀的类? 在 Kotlin 中, 根本不需要去创建这些无意义的类, 你可以把这些函数直接放到代码文件的顶层, 不用从属于任何类事实上 joinToString 函数之前就是直接定义在 Join.kt 文件
- package com.huburt.imagepicker
- @JvmOverloads
- fun <T> joinToString(...): String {...}
这会怎样运行呢? 当编译这个文件的时候, 会生成一些类, 因为 JVM 只能执行类中的代码当你在使用 Kotlin 的时候, 知道这些就够了但是如果你需要从 Java 中来调用这些函数, 你就必须理解它将怎样被编译, 来看下编译后的类是怎样的:
- package com.huburt.imagepicker
- public class JoinKt {
- public static String joinToString(...){...}
- }
可以看到 Kotlin 编译生成的类的名称, 对应于包含函数的文件名称, 这个文件中的所有顶层函数编译为这个类的静态函数因此, 当从 Java 调用这个函数的时候, 和调用任何其他静态函数一样简单:
- import com.huburt.imagepicker.JoinKt
- JoinKt.joinToString(...)
修改文件类名
是不是觉得 Kt 结尾的类使用起来很别扭, Kotlin 提供了方法改变生成类的类名, 只需要为这个 kt 文件添加 @JvmName 的注解, 将其放到这个文件的开头, 位于包名的前面:
- @file:JvmName("Join") // 指定类名
- package com.huburt.imagepicker
- @JvmOverloads
- fun <T> joinToString(...): String {...}
现在就可以用新的类名调用这个函数:
- import com.huburt.imagepicker.Join
- Join.joinToString(...)
顶层属性
和函数一样, 属性也可以放到文件的顶层从 Java 的角度来看就是静态属性, 没啥特别的, 而且由于没有了类的存在, 这种属性用到的机会也不多 需要注意的是顶层函数和其他任意属性一样, 默认是通过访问器暴露给 Java 使用的 (也就是通过 getter 和 setter 方法) 为了方便使用, 如果你想要把一个常量以
public static final
的属性暴露给 Java, 可以用 const 来修饰属性:
const val TAG = "tag"
这样就等同与 Java 的:
public static final String TAG = "tag"
给别人的类添加方法: 扩展函数和属性
Kotlin 的一大特色就是可以平滑的与西安欧代码集成你可以完全在原有的 Java 代码基础上开始使用 Kotlin 对于原有的 Java 代码可以不修改源码的情况下扩展功能: 扩展函数这一点是我认为 Kotlin 最强大的地方了 扩展函数非常简单, 它就是一个类的成员函数, 不过定义在类的外面为了方便阐述, 让我们添加一个方法, 来计算一个字符串的最后一个字符:
- package strings
- //String ->接收者类型 //this ->接收者类型
- fun String.lastChar(): Char = this.get(this.length - 1)
你所要做的, 就是把你要扩展的类或者接口的名称, 放到即将添加的函数前面, 这个类的名称被称为接收者类型; 用来调用这个扩展函数的那个对象, 叫做接收者对象 接着就可以像调用类的普通成员函数一样去调用这个函数了:
- println("Kotlin".lastChar())
- // 输出
- n
在这个例子中, String 就是接收者类型, 而 Kotlin 就是接收者对象 在这个扩展函数中, 可以像其他成员函数一样用 this, 也可以像普通函数一样省略它:
- package strings
- fun String.lastChar(): Char = get(length - 1) // 省略 this 调用 string 对象其他函数
导入扩展函数
对于你定义的扩展函数, 它不会自动的在整个项目范围内生效如果你需要使用它, 需要进行导入, 导入单个函数与导入类的语法相同:
- import strings.lastChar
- val c = "Kotlin".lastChar()
当然也可以用 * 表示文件下所有内容: import strings.*
另外还可以使用 as 关键字来修改导入的类或则函数的名称:
- import strings.lastChar as last
- val c = "Kotlin".last()
在导入的时候重命名可以解决函数名重复的问题
从 Java 中调用扩展函数
实际上, 扩展函数是静态函数, 它把调用对象作为函数的第一个参数在 Java 中调用扩展函数和其他顶层函数一样, 通过. kt 文件生成 Java 类调用静态的扩展函数, 把接收者对象传入第一个参数即可例如上面提到的 lastChar 扩展函数是定义在 StringUtil.kt 中, 在 Java 中就可以这样调用:
char c = StringUtilKt.lastChar("Java")
作为扩展函数的工具函数
现在我们可以写一个 joinToString 函数的终极版本了, 它和你在 Kotlin 标准库中看到的一模一样:
- @JvmOverloads
- fun <T> Collection<T>.joinToString(
- separator: String = ",",
- prefix: String = "",
- postfix: String = ""
- ): String {
- val result = StringBuilder(prefix)
- for ((index, element) in this.withIndex()) { //this 是接收者对象, 即 T 的集合
- if (index> 0) result.append(separator)
- result.append(element)
- }
- result.append(postfix)
- return result.toString()
- }
- // 使用
- println(list.joinToString())
- println(list.joinToString(";"))
- println(list.joinToString(prefix = "{", postfix = "}"))
将原来的参数 Collection, 提出来, 作为接收者类型编写的扩展函数, 使用方法也像是 Collection 类的成员函数一样了(当然 Java 调用还是静态方法, 第一个参数传入 Collection 对象)
不可重写的扩展函数
先来看一个重写的例子:
- //Kotlin 中 class 默认是 final 的, 如果需要继承需要修饰 open, 函数也相同
- open class View {
- open fun click() = println("View clicked")
- }
- class Button : View() { // 继承
- override fun click() = println("Button clicked")
- }
当你声明了类型为 View 的变量, 那它可以被赋值为 Button 类型的对象, 因为 Button 是 View 的一个子类当你在调用这个变量的一般函数, 比如 click 的时候, 如果 Button 复写了这个函数, name 这里将会调用到 Button 中复写的函数:
- val view: View = Button()
- view.click()
- // 输出
- Button clicked
但是对于扩展函数来说, 并不是这样的扩展函数并不是类的一部分, 它是声明在类之外的尽管可以给基类和子类都分别定义一个同名的扩展函数, 当这个函数被调用时, 它会用到哪一个呢? 这里, 它是由该变量的静态类型所决定的, 而不是这个变量的运行时类型
- fun View.showOff() = println("i'm a view!")
- fun Button.showOff() = println("i'm a button!")
- val view: View = Button()
- view.click()
- // 输出
- i'm a view!
当你在调用一个类型为 View 的变量的 showOff 函数时, 对应的扩展函数会被调用, 尽管实际上这个变量现在是一个 Button 对象回想一下, 扩展函数会在 Java 中编译为静态函数, 同时接受值将会作为第一个参数这样其实 2 个 showOff 扩展函数就是不同参数的静态函数,
- View view = new Button();
- XxKt.showOff(view); // 定义在 Xx.kt 文件中
参数的类型决定了调用那个静态函数, 想要调用 Button 的扩展函数, 则必须先将参数转成 Button 类型才行:
XxKt.showOff((Button)view);
因此, 扩展函数也是有局限性的, 扩展函数是能扩展, 即定义新的函数, 而不能重写改变原有函数的实现 (本质是一个静态函数) 如果定了一个类中本身存在成员函数同名的扩展函数, Kotlin 种调用该方法的时候会如何呢?(Java 中没有这个顾虑, 调用方式不同)
- open class View {
- open fun click() = println("View clicked")
- }
- fun View.click() = println("扩展函数")
- val view = View()
- view.click()
- // 输出
- View clicked
明显了吧~ 对于有同名成员函数和扩展函数时, 在 Kotlin 中调用始终执行成员函数的代码, 扩展函数并不起作用, 相当于没有定义这一点在实际开发中需要特别注意了!
扩展属性
扩展属性提供了一种方法, 用于扩展类的 API, 可以用来访问属性, 用的是属性语法而不是函数的语法尽管他们被称为属性, 但它们可以没有任何状态, 因为没有合适的地方来存储它, 不可能给现有的 Java 对象的实例添加额外的字段举个例子吧:
- val String.lastChar: Char
- get() = get(length - 1)
同样是获取字符串的最后一个字符, 这次是用扩展属性的方式定义扩展属性也像接收者的一个普通成员属性一样, 这里必须定义 getter 函数, 因为没有支持字段, 因此没有默认的 getter 的实现同理, 初始化也不可以: 因为没有地方存储初始值 刚刚定义是一个 val 的扩展属性, 也可以定义 var 属性:
- var StringBuilder.lastChar: Char
- get() = get(length - 1)
- set(value) {
- setCharAt(length - 1, value)
- }
还记得上一篇文章的自定义访问器的内容吗? 这里的定义方式与自定义访问器一致, val 属性不可变, 因此只需要定义 getter, 而 var 属性可变, 所以 getter 和 setter 都需要 可能不是很好理解扩展属性, 或者会和真正的属性混淆, 下面列出了扩展属性转换成 Java 的代码, 你就会比较直观的理解了
- public static final char getLastChar(@NotNull String $receiver) {
- return $receiver.charAt($receiver.length() - 1);
- }
- public static final char getLastChar(@NotNull StringBuilder $receiver) {
- return $receiver.charAt($receiver.length() - 1);
- }
- public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
- $receiver.setCharAt($receiver.length() - 1, value);
- }
和扩展函数是相同的, 仅仅是静态函数: 提供获取 lastChar 的功能, 这样的定义方式可以在 Kotlin 中像使用普通属性的调用方式来使用扩展属性, 给你一种这是属性的感觉, 但本质上在 Java 中就是静态函数
处理集合: 可变参数中缀调用和库的支持
扩展 Java 集合的 API
- val strings = listOf("first", "second", "fourteenth")
- println(strings.last())
- val numbers = setOf(1, 14, 2)
- println(numbers.max())
还记的之前我们使用上面的方式获取了 list 的最后一个元素, 以及 set 中的最大值到这里你可能已经知道了, last() 和 max() 都是扩展函数, 自己点进方法验证一下吧!
可变参数
如果你也看了 listOf 函数的定义, 你一定看到了这个:
public fun <T> listOf(vararg elements: T): List<T>
也就是 vararg 关键字, 这让函数支持任意个数的参数在 Java 中同样的可变参数是在类型后面跟上... , 上面的方法在 Java 则是:
public <T> List<T> listOf(T... elements)
但是 Kotlin 的可变参数相较于 Java 还是有点区别: 当需要传递的参数已经包装在数组中时, 调用该函数的语法, 在 Java 中可以按原样传递数组, 而 Kotlin 则要求你显示地解包数组, 以便每个数组元素在函数中能作为单独的参数来调用从技术的角度来讲, 这个功能被称为展开运算符, 而使用的时候, 不过是在对应的参数前面放一个 * :
- val array = arrayOf("a", "b")
- val list = listOf("c", array)
- println(list)
- val list2 = listOf<String>("c", *array)
- println(list2)
- // 输出
- [c, [Ljava.lang.String;@5305068a]
- [c, a, b]
通过对照可以看到, 如果不加 * , 其实是把数组对象当做了集合的元素加上 * 才是将数组中所有元素添加到集合中 listOf 也可以指定泛型 < String> , 你可以尝试在 listOf("c", array) 这里加泛型, 第二个参数 array 就会提示类型不正确
Java 中没有展开, 我们也可以调用 Kotlin 的 listOf 函数, 该函数声明在 Collections.kt 文件下:
- List<String> strings = CollectionsKt.listOf(array);
- System.out.println(strings);
- //List<String> strings = CollectionsKt.listOf("c", array);// 无法编译
- // 输出
- [a, b]
Java 中可以直接传入数组, 但是不能同时传入单个元素和数组
键值对的处理: 中缀调用和解构声明
还记得创建 map 的方式吗?
val map = mapOf(1 to "one", 7 to "seven", 52 to "fifty-five")
之前说过 to 并不是一个内置的结构, 而是一种特殊的函数调用, 被称为中缀调用 在中缀调用中, 没有添加额外的分隔符, 函数名称是直接放在目标对象名称和参数之间的, 以下两种调用方式是等价的:
- 1.to("one")// 普通调用
- 1 to "one" // 中缀调用
中缀调用可以与只有一个参数的函数一起使用, 换句话说就是只要函数只有一个参数, 都可以支持在 Kotlin 中的中缀调用, 无论是普通的函数还是扩展函数要允许使用中缀符号调用函数, 需要使用 infix 修饰符来标记它例如 to 函数的声明:
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
to 函数会返回一个 Pair 类型的对象, Pair 是 Kotlin 标准库中的类, 它是用来表示一对元素我们也可以直接用 Pair 的内容来初始化两个变量:
val (number, name) = 1 to "one"
这个功能称之为解构声明, 1 to "one" 会返回一个 Pair 对象, Pair 包含一对元素, 也就是 1 和 one, 接着又定义了变量(number, name) 分别指向 Pair 中的 1 和 one 解构声明特征不止用于 Pair 还可以使用 map 的 key 和 value 内容来初始化两个变量并且还适用于循环, 正如你在使用的 withIndex 函数的 joinToString 实现中看到的:
- for ((index, element) in collection.withIndex()) {
- printLn("$index, $element")
- }
to 函数是一个扩展函数, 可以创建一对任何元素, 这意味着它是泛型接受者的扩展: 可以使用 1 to "one" "one" to 1
list to list.size()
等写法我们来看看 mapOf 函数的声明:
public fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>
与 listOf 一样, mapOf 接受可变数量的参数, 但这次他们应该是键值对尽管在 Kotlin 中创建 map 可能看起来像特殊的解构, 而它不过是一个具有简明语法的常规函数
字符串和正则表达式的处理
Kotlin 定义了一系列扩展函数, 使标准 Java 字符串使用起来更加方便
分割字符串
Java 中我们会使用 String 的 split 方法分割字符串但有时候会产生一些意外的情况, 例如当我们这样写
"12.345-6.A".split(".")
的时候, 我们期待的结果是得到一个 [12, 345-6, A] 数组但是 Java 的 split 方法竟然返回一个空数组! 这是应为它将一个正则表达式作为参数, 并根据表达式将字符串分割成多个字符串这里的点 (.) 是表示任何字符的正则表达式 在 Kotlin 中不会出现这种令人费解的情况, 因为正则表达式需要一个 Regex 类型承载, 而不是 String 这样确保了字符串不会被当做正则表达式
- println("12.345-6.A".split("\\.|-".toRegex())) // 显示地创建一个正则表达式
- // 输出
- [12, 345, 6, A ]
这里正则表达式语法与 Java 的完全相同, 我们匹配一个点 (对它转义表示我们指的时字面量) 或者破折号 对于一些简单的情况, 就不需要正则表达式了, Kotlin 中的 spilt 扩展函数的其他重载支持任意数量的纯文本字符串分隔符:
println("12.345-6.A".split(".", "-")) // 指定多个分隔符
等同于上面正则的分割
正则表达式和三重引号的字符串
现在有这样一个需求: 解析文件的完整路径名称
/Users/hubert/kotlin/chapter.adoc
到对应的组件: 目录文件名扩展名 Kotlin 标准库中包含了一些可以用来获取在给定分隔符第一次 (或最后一次) 出现之前 (或之后) 的子字符串的函数
- val path = "/Users/hubert/kotlin/chapter.adoc"
- val directory = path.substringBeforeLast("/")
- val fullName = path.substringAfterLast("/")
- val fileName = fullName.substringBeforeLast(".")
- val extension = fullName.substringAfterLast(".")
- println("Dir: $directory, name: $fileName, ext: $extension")
- // 输出
- Dir: /Users/hubert/kotlin, name: chapter, ext: adoc
解析字符串在 Kotlin 中变得更加容易, 但如果你仍然想使用正则表达式, 也是没有问题的:
- val regex = """(.+)/(.+)\.(.+)""".toRegex()
- val matchResult = regex.matchEntire(path)
- if (matchResult != null) {
- val (directory, fileName, extension) = matchResult.destructured
- println("Dir: $directory, name: $fileName, ext: $extension")
- }
这里正则表达式写在一个三重引号的字符串中在这样的字符串中, 不需要对任何字符进行转义, 包括反斜线, 所以可以用 \. 而不是 \\. 来表示点, 正如写一个普通字符串的字面值在这个正则表达式中: 第一段(.+) 表示目录,/ 表示最后一个斜线, 第二段(.+) 表示文件名,\. 表示最后一个点, 第三段(.+) 表示扩展名
多行三重引号的字符串
三重引号字符串的目的, 不仅在于避免转义字符, 而且使它可以包含任何字符, 包括换行符它提供了一种更简单的方法, 从而可以简单的把包含换行符的文本嵌入到程序中:
- val kotlinLogo = """|//
- .|//
- .|/ \
- """.trimMargin(".")
- print(kotlinLogo)
- // 输出
- |//
- |//
- |/ \
多行字符串包含三重引号之间的所有字符, 包括用于格式化代码的缩进如果要更好的表示这样的字符串, 可以去掉缩进 (左边距) 为此, 可以向字符串内容添加前缀, 标记边距的结尾, 然后调用 trimMargin 来删除每行中的前缀和前面的空格在这个例子中使用了. 来作为前缀
让你的代码更整洁: 局部函数和扩展
许多开发人员认为, 好代码的重要标准之一就是减少重复代码 Kotlin 提供了局部函数来解决常见的代码重复问题下面的例子中是在将 user 的信息保存到数据库前, 对数据进行校验的代码:
- class User(val id: Int, val name: String, val address: String)
- fun saveUser(user: User) {
- if (user.name.isEmpty()) {
- throw IllegalArgumentException("Can't save user ${user.id}:empty Name")
- }
- if (user.address.isEmpty()) {
- throw IllegalArgumentException("Can't save user ${user.id}:empty Name")
- }
- // 保存 user 到数据库
- }
分别对每个属性校验的代码就是重复的代码, 特别当属性多的时候就重复的更多这种时候将验证的代码放到局部函数中, 可以摆脱重复同时保持清晰的代码结构局部函数, 顾名思义就是定义在函数中的函数我们使用局部函数来改造上面这个例子:
- class User(val id: Int, val name: String, val address: String)
- fun saveUser(user: User) {
- // 声明一个局部函数
- fun validate(value: String, fieldName: String) {
- if (value.isEmpty()) {
- // 局部函数可以直接访问外部函数的参数: user
- throw IllegalArgumentException("Can't save user ${user.id}:empty $fieldName")
- }
- }
- validate(user.name,"Name")
- validate(user.address,"Address")
- // 保存 user 到数据库
- }
我们还可以继续改进, 将逻辑提取到扩展函数中:
- class User(val id: Int, val name: String, val address: String)
- fun User.validateBeforeSave() {
- fun validate(value: String, fieldName: String) {
- if (value.isEmpty()) {
- throw IllegalArgumentException("Can't save user $id:empty $fieldName")
- }
- }
- validate(name, "Name")// 扩展函数直接访问接收者对象 user 的属性
- validate(address, "Address")
- }
- fun saveUser(user: User) {
- user.validateBeforeSave()
- // 保存 user 到数据库
- }
来源: https://juejin.im/post/5ab20ac8f265da23826ddc6a