本文首发于 vivo 互联网技术 微信公众号
链接:https://mp.weixin.qq.com/s/UV23Uw_969oVhiOdo4ZKAw
作者:连凌能
Kotlin,已经被 Android 官方宣布 kotlin first 的存在,去翻 Android 官方文档的时候,发现提供的示例代码已经变成了 Kotlin。Kotlin 的务实作风,提供了很多特性帮助开发者减少冗余代码的编写,可以提高效率,也能减少异常。
本文简单谈下 Kotlin 中的函数,包括表达式函数体,命名参数,默认参数,顶层函数,扩展函数,局部函数,Lambda 表达式,成员引用,with/apply 函数等。从例子入手,从一般写法到使用特性进行简化,再到原理解析。
1. 表达式函数体
通过下面这个简单的例子看下函数声明相关的概念,函数声明的关键字是 fun,嗯,比 JS 的 function 还简单。
Kotlin 中参数类型是放在变量:后面,函数返回类型也是。
fun max(a: Int, b: Int) : Int {if (a > b) {return a} else {return b}
}
当然,Kotlin 是有类型推导功能,如果可以根据函数表达式推导出类型,也可以不写返回类型。
但是上面的还是有点繁琐,还能再简单,在 Kotlin 中 if 是表达式,也就是有返回值的,因此可以直接 return,另外判断式中只有一行一句也可以省略掉大括号:
fun max(a: Int, b: Int) {return if (a > b) a else b
}
还能在简单点吗?可以,if 是表达式,那么就可以通过表达式函数体返回:
fun max(a: Int, b: Int) = if(a > b) a else b
最终只需要一行代码。
Example
再看下面这个例子,后面会基于这个例子进行修改。这个函数把集合以某种格式输出,而不是默认的 toString()。
<T> 是泛型,在这里形参集合中的元素都是 T 类型。返回 String 类型。fun <T> joinToString(
collection: Collection<T>,
separator: String,
prefix: String,
postfix: String
): String {val sb = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {if (index > 0) sb.append(separator)
sb.append(element)
}
sb.append(postfix)
return sb.toString()}
2. 命名参数调用
先来看下函数调用,相比 Java, Kotlin 中可以类似于 JavaScript 中带命名参数进行调用,而且可以不用按函数声明中的顺序进行调用,可以打乱顺序,比如下面:
joinToString(separator = "", collection = list, postfix ="}", prefix ="{")
// example
val list = arrayListOf("10", "11", "1001")
println(joinToString(separator = "", collection = list, postfix ="}", prefix ="{"))
>>> {10 11 1001}
3. 默认参数
Java 里面有重载这一说,或者 JavaScript 有默认参数值这一说,Kotlin 采用了默认参数值。调用的时候就不需要给有默认参数值的形参传实参。上面的函数改成如下:
fun <T> joinToString(
collection: Collection<T>,
separator: String = " ",
prefix: String = "[",
postfix: String = "]"
): String {...}
//
joinToString(list)
那么调用的时候如果默认参数值自己的满足要求,就可以只传入集合 list 即可。
4. 顶层函数
不同于 Java 中函数只能定义在每个类里面,Kotlin 采用了 JavaScript 中的做法,可以在文件任意位置处定义函数,这种函数称为顶层函数。
编译后顶层函数会成为文件类下的静态函数,比如在文件名是 join.kt 下定义的 joinToString 函数可以通过 JoinKt.joinToSting 调用,其中 JoinKt 是编译后的类名。
// 编译成静态函数
// 文件名 join.kt
package strings
fun joinToString() : String {...}
/* Java */
import strings.JoinKt;
JoinKt.joinToSting(....)
看下上面函数编译后的效果:// 编译成 class 文件后反编译结果
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix, @NotNull String postfix) {Intrinsics.checkParameterIsNotNull(collection, "collection");
Intrinsics.checkParameterIsNotNull(separator, "separator");
Intrinsics.checkParameterIsNotNull(prefix, "prefix");
Intrinsics.checkParameterIsNotNull(postfix, "postfix");
StringBuilder sb = new StringBuilder(prefix);
int index = 0;
for(Iterator var7 = ((Iterable)collection).iterator(); var7.hasNext(); ++index) {Object element = var7.next();
if (index > 0) {sb.append(separator);
}
sb.append(element);
}
sb.append(postfix);
String var10000 = sb.toString();
Intrinsics.checkExpressionValueIsNotNull(var10000, "sb.toString()");
return var10000;
}
// 默认函数值
public static String joinToString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {if ((var4 & 2) != 0) {var1 = " ";}
if ((var4 & 4) != 0) {var2 = "[";}
if ((var4 & 8) != 0) {var3 = "]";
}
return joinToString(var0, var1, var2, var3);
接下来看下 Kotlin 中很重要的一个特性,扩展函数。
5. 扩展函数
- 扩展函数是类的一个成员函数,不过定义在类的外面
- 扩展函数不能访问私有的或者受保护的成员
- 扩展函数也是编译成静态函数
所以可以在 Java 库的基础上通过扩展函数进行封装,假装好像都是在调用 Kotlin 自己的库一样,在 Kotlin 中 Collection 就是这么干的。
再对上面的 joinToString 来一个改造,终结版:
fun <T> Collection<T>.joinToString(
separator: String = " ",
prefix: String = "[",
postfix: String = "]"
): String {val sb = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {if (index > 0) sb.append(separator)
sb.append(element)
}
sb.append(postfix)
return sb.toString()}
在这里声明成了 Collection 接口类的扩展函数,这样就可以直接通过 list 进行调用, 在扩展函数里面照常可以使用 this,这里的 this 就是指向接收者对象,在这里就是 list。
val list = arrayListOf("10", "11", "1001")
println(list.joinToString())
>>> [10 11 1001]
经常我们需要对代码进行重构,其中一个重要的措施就是减少重复代码,在 Java 中可以抽取出独立的函数,但这样有时候对整体结构并不太好,Kotlin 提供了局部函数来解决这个问题。
6. 局部函数
顾名思义,局部函数就是可以在函数内部定义函数。先看下没有使用局部函数的一个例子,这个例子先对传进来的用户名和地址进行校验,只有都不为空的情况下才存进数据库:
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 Address")
}
// Save user to the database
}
上面有重复的代码,就是对 name 和 address 的校验重复了,只是入参的不同,因此可以抽出一个校验函数,使用局部函数重写:
fun saveUser(user: User) {fun validate(value: String, fieldName: String) {if (value.isEmpty()) {
throw IllegalArgumentException("Can't save user ${user.id}: empty $fieldName")
}
}
validate(user.name, "Name")
validate(user.address, "Address")
}
布局函数可以访问所在函数中的所有参数和变量。
如果不支持 Lambda 都不好意思称自己是一门现代语言,来看看 Kotlin 中的表演。
7.Lambda 表达式
Lambda 本质上是可以传递给其他函数的一小段代码,可以当成值到处传递
Lambda 表达式以左大括号开始,以右大括号结束,箭头 -> 分割成两边,左边是入参,右边是函数体。
val sum = {x : Int, y : Int -> x + y}
println(sum(1, 2))
// 可以直接 run
run {println(42)}
如果 Lambda 表达式是函数调用的最后一个实参,可以放到括号外边;
当 Lambda 是函数唯一实参时,可以去掉调用代码中的空括号;
和局部变量一样,如果 Lambda 参数的类型可以被推导出来,就不需要显示的指定。
val people = listOf(User(1, "A", "B"), User(2, "C", "D"))
people.maxBy {it.id}
如果在函数内部使用 Lambda, 可以访问这个函数的参数,还有在 Lambda 之前定义的局部变量。
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {if (it.startsWith("4")) {clientErrors++} else if (it.startsWith("5")) {serverErrors++}
}
println("$clientErrors client errors, $serverErrors server errors")
}
考虑这么一种情况,如果一个函数 A 接收一个函数类型参数,但是这个参数功能已经在其它地方定义成函数 B 了,有一种办法就是传入一个 Lambda 表达式给 A,在这个表达式中调用 B,但是这样就有点繁琐了,有没有可以直接拿到 B 的方式呢?
我都说了这么多了,肯定是有了。。。那就是成员引用。
8. 成员引用
如果 Lambda 刚好是函数或者属性的委托,可以用成员引用替换。
people.maxBy(User::id)
Ps:不管引用的是函数还是属性,都不要在成员引用的名称后面加括号
引用顶层函数
fun salute() = println("Salute!")
run(::salute)
如果 Lambda 要委托给一个接收多个参数的函数,提供成员引用代替会非常方便:fun sendEmail(person: Person, message: String) {
println("message: $message")
}
val action = { person: Person, message: String ->
sendEmail(person, message)
}
// action 可以简化如下
val action = ::sendEmail
//
action(p, "HaHa")
可以用 构造方法引用 存储或者延期执行创建类实例的动作,构造方法的引用的形式是在双冒号后指定类名称:
data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
还可以用同样的方式引用扩展函数。
fun Person.isAdult() = age>= 21
val predicate = Person::isAdult
不看点稍微底层的,就显得不够专业,逼格不够,接下来稍微探究下 Lambda 的原理。
9.Lambda 表达式原理
自 Kotlin 1.0 起,每个 Lambda 表达式都会被编译成一个匿名类,除非它是一个内联 Lambda。后续版本计划支持生成 Java 8 字节码,一旦实现,编译器就可以避免为每一个 lambda 表达式都生成一个独立的.class 文件。
如果 Lambda 捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次调用都会创建一个这个匿名类的新实例。否则,一个单例就会被创建。类的名称由 Lambda 声明所在的函数名称加上后缀衍生出来,这个例子中就是 TestLambdaKt$main$1.class。
// TestLambda.kt
package ch05
fun salute(callback: () -> Unit) = callback()
fun main(args: Array<String>) {salute { println(3) }
}
编译后,生成两个文件。
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2019/7/24 14:33 1239 TestLambdaKt$main$1.class
-a---- 2019/7/24 14:35 1237 TestLambdaKt.class
先看下 TestLambdaKt$main$1.class,构造一个静态实例 ch05.TestLambdaKt$main$1 INSTANCE, 在类加载的时候进行赋值,同时继承接口 Function0, 实现 invoke 方法:
final class ch05.TestLambdaKt$main$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit>
minor version: 0
major version: 50
flags: ACC_FINAL, ACC_SUPER
Constant pool:...
{
public static final ch05.TestLambdaKt$main$1 INSTANCE;
descriptor: Lch05/TestLambdaKt$main$1;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
public java.lang.Object invoke();
descriptor: ()Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #12 // Method invoke:()V
4: getstatic #18 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
7: areturn
public final void invoke();
descriptor: ()V
flags: ACC_PUBLIC, ACC_FINAL
Code:
stack=2, locals=2, args_size=1
0: iconst_3
1: istore_1
2: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;
5: iload_1
6: invokevirtual #30 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 6: 0
line 6: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lch05/TestLambdaKt$main$1;
ch05.TestLambdaKt$main$1();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_0
2: invokespecial #35 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
5: return
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #2 // class ch05/TestLambdaKt$main$1
3: dup
4: invokespecial #56 // Method "<init>":()V
7: putstatic #58 // Field INSTANCE:Lch05/TestLambdaKt$main$1;
10: return
}
再看下另外一个类 TestLambdaKt.class, 在 main 方法中传入 TestLambdaKt$main$1.INSTANCE 给方法 salute, 在方法 salute 中调用接口方法 invoke,见上面。
public final class ch05.TestLambdaKt
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
...
{public static final void salute(kotlin.jvm.functions.Function0<kotlin.Unit>);
descriptor: (Lkotlin/jvm/functions/Function0;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #10 // String callback
3: invokestatic #16 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: aload_0
7: invokeinterface #22, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
12: pop
13: return
LineNumberTable:
line 3: 6
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 callback Lkotlin/jvm/functions/Function0;
Signature: #7 // (Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;)V
RuntimeInvisibleParameterAnnotations:
0:
0: #8()
public static final void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: ldc #27 // String args
3: invokestatic #16 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
6: getstatic #33 // Field ch05/TestLambdaKt$main$1.INSTANCE:Lch05/TestLambdaKt$main$1;
9: checkcast #18 // class kotlin/jvm/functions/Function0
12: invokestatic #35 // Method salute:(Lkotlin/jvm/functions/Function0;)V
15: return
LineNumberTable:
line 6: 6
line 7: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
RuntimeInvisibleParameterAnnotations:
0:
0: #8()}
Ps:Lambda 内部没有匿名对象那样的的 this:没有办法引用到 Lambda 转换成的匿名类实例。从编译器角度看,Lambda 是一个代码块不是一个对象,不能把它当成对象引用。Lambda 中的 this 引用指向的是包围它的类。
如果在 Lambda 中要用到常规意义上 this 呢?这个就需要带接收者的函数。看下比较常用的两个函数 with 和 apply。
10.with 函数
直接上 Kotlin 的源码,with 在这里声明成内联函数 (后面找机会说),接收两个参数,在函数体里面对接收者调用 Lambda 表达式。在 Lambda 表达式里面可以通过 this 引用到这个 receiver 对象。
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
看个例子:
fun alphabet(): String {val result = StringBuilder()
for (letter in 'A'..'Z') {result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()}
with 改造,在 with 里面就不用显示通过 StringBuilder 进行 append 调用。
fun alphabet(): String {val result = StringBuilder()
return with(result) {for (letter in 'A'..'Z') {append(letter)
}
append("\nNow I know the alphabet!")
this.toString()}
}
// 再进一步
fun alphabet() = with(StringBuilder()) {for (letter in 'A'..'Z') {append(letter)
}
append("\nNow I know the alphabet!")
toString()}
with 返回的值是执行 Lambda 代码的结果,该结果是 Lambda 中的最后一个表达式的值。如果想返回的是接收者对象,而不是执行 Lambda 的结果,需要用 apply 函数。
11.apply 函数
apply 函数几乎和 with 函数一模一样,唯一的区别就是 apply 始终返回作为实参传递给它的对象,也就是接收者对象。
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {block(); return this }
apply 被声明称一个扩展函数,它的接收者变成了作为实参传入的 Lambda 的接收者。
fun alphabet() = StringBuilder().apply {for (letter in 'A'..'Z') {append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
可以调用库函数再简化:
fun alphabet() = buildString {for (letter in 'A'..'Z') {append(letter)
}
append("\nNow I know the alphabet!")
}
//
/**
* Builds new string by populating newly created [StringBuilder] using provided [builderAction]
* and then converting it to [String].
*/
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
12. 总结
本文只是说了 Kotlin 中关于函数的一点特性,当然也没讲全,比如内联函数,高阶函数等,因为再写下去太长了,所以后面再补充。从上面几个例子也能大概感受到 Kotlin 的务实作风,提供了很多特性帮助开发者减少冗余代码的编写,可以提高效率,也能减少异常,让程序猿早点下班,永葆头发乌黑靓丽。
更多内容敬请关注 vivo 互联网技术 微信公众号
注:转载文章请先与微信号:labs2020 联系。