稍微也提一下Scope函数
在Kotlin里面,经常会用到Scope函数,在一个值上面创建一个临时的作用域来进行各种操作。更具体的说,这是一类定义在任意类型上的扩展函数,接受一个以这个类型作为参数的lambda回调,这样的话,这个值就可以被「注入」进作用域,来实现更简洁的声明式写法了。
Scope函数一般来说有这么几种主要的使用场合:
和可空类型还有nullable调用链结合,去掉繁琐的precondition
比如说命令式常见的检测if的代码例如这个:
1
2
3if (canBeNull != null) {
doSomething(canBeNull)
}就可以被改写成这样:
1
canBeNull?.let { doSomething(it) }
甚至:
1
canBeNull?.let(::doSomething)
在涉及到多个可空变量或者连续调用的情况下,Scope函数的写法显然更简洁清晰,更「Kotlinic」。
提供一种简洁的声明式链式调用的风格
假设一种Java里常见的数据提取过程:
1
2
3
4
5
6
7
8
9
10
11try (ResultSet resultSet = stmt.executeQuery(selectSql)) {
List<Employee> employees = new ArrayList<>();
while (resultSet.next()) {
Employee emp = new Employee();
emp.setId(resultSet.getInt("emp_id"));
emp.setName(resultSet.getString("name"));
emp.setPosition(resultSet.getString("position"));
emp.setSalary(resultSet.getDouble("salary"));
employees.add(emp);
}
}同样的代码在Kotlin里面可以被改造成这样:
1
2
3
4
5
6
7
8
9
10
11
12stmt.executeQuery(selectSql).use { resultSet ->
val employees = mutableListOf<Employee>()
while (resultSet.next()) {
employees.add(Employee().apply {
id = resultSet.getInt("emp_id")
name = resultSet.getString("name")
position = resultSet.getString("position")
salary = resultSet.getDouble("salary")
})
}
...
}注意到这里同时结合了
apply
和use
两种不同的Scope函数。因为emp
变量被隐含进了上下文,代码看起来简洁很多,而且变量字段的修改被限定进了apply
函数里,也避免影响了外层作用域,减轻代码的心智负担。use
可以在Kotlin里面实现类似C++或C#的RAII的机制,这个之后会提到。如果要把Kotlinic延伸到极致的话,这段代码还可以进一步改造,去掉while循环,当然这里就先不讨论了。
总的来说,这种写法可以算用更Kotlin的方式解决Java中常见的「创建一个空对象然后多次对字段赋值」的命令式模式。
避免耗时运算的多次调用
另一个例子就是在调用到耗时比较长的函数的时候,例如:
1
2
3if (longComputation() != null && longComputation().result > 10) {
println("The value is ${longComputation().result}")
}同样的代码等价于这样的写法:
1
2
3
4
5longComputation()?.let {
if (it > 10) {
println("The value is $it")
}
}当然有人可以说可以在外面提前把运算计算完存进变量,那么实际上Scope函数做的是同一件事情,与此同时,
it
的值只能在Scope函数内部被调取,可以减少对外层作用域的干扰。这也是Scope的含义,就是提供一个作用域。模拟类似RAII的语法
前面提到的
use
就是一种Scope函数,同时也起到了模拟RAII的语义。大概的作用是,不需要显式地关闭Closeable
的资源,而是让use
函数来接管资源的关闭并且处理可能的异常。use
的语法其实很像Java的try-with-resources
语句,不过不同的是,比起创建一种新的语法,Kotlin里面只是简单地复用了函数作用和Scope函数的语法,降低了学习成本。模拟例如C#等语言里的
??=
语义C#里面,
??=
允许把一个值赋予给可空的变量,如果变量为空。大概是这样:1
ref ??= "backingValue"
Kotlin里面的话,一般是用
takeIf
或者run
来模拟的:1
2ref = ref.takeIf { it != null } ?: "backingValue"
ref = ref.run { this ?: "backingValue" }当然,这种写法其实不太直观,而且也有点hacking的味道。一般来说,大部分人会想到的也就是把赋值语句包含在if子句里了。Kotlin在这方面比起C#,还是有所欠缺的。
本质上,Scope函数是一种定义在泛型类型上的扩展函数,换句话说,这些函数在支持扩展函数语法和泛型的语言上一般都是可以定义出来的。let
最简单的写法大概可以是这样:
1 |
|
不过反编译一下的话,会发现可能没那么简单:
1 |
|
可以发现这里多了几个对象的创建。
实际上,Kotlin的标准库函数也确实很简单,抛开contract
(用来保证函数调用的precondition的,不影响函数实际运行)不提的话,只是多了inline
关键字和InlineOnly
注解。意思是把block
这个lambda直接内联进调用处的函数。
1 |
|
对比以下的代码和编译结果也可以看出,函数调用的开销被消除了:
1 |
|
1 |
|
当然,如果过度依赖内联函数的话,生成的字节码也会急剧膨胀。不过一般来说, 对于这种依赖lambda的函数进行内联是没问题的。
这也说明了另一个问题,在没有内联函数或等同的语言机制的其他语言里,直接照搬Scope函数的写法,可能会导致性能的明显下降。
最后也顺便做一下测试好了,用JMH来对比直接调用和套了一层let的区别。虽然刚才得出的结论应该是它们没区别。
测试代码如下:
1 |
|
测试结果:
Benchmark | (size) | (type) | Mode | Cnt | Score | Error | Units |
---|---|---|---|---|---|---|---|
ScopeBenchmark.functionCallByInvoke | 100 | ArrayList | avgt | 10 | 0.319 | ± 0.001 | us/op |
ScopeBenchmark.functionCallByInvoke | 100 | LinkedList | avgt | 10 | 0.321 | ± 0.004 | us/op |
ScopeBenchmark.functionCallByInvoke | 200 | ArrayList | avgt | 10 | 0.641 | ± 0.007 | us/op |
ScopeBenchmark.functionCallByInvoke | 200 | LinkedList | avgt | 10 | 0.679 | ± 0.007 | us/op |
ScopeBenchmark.functionCallByInvoke | 500 | ArrayList | avgt | 10 | 1.567 | ± 0.008 | us/op |
ScopeBenchmark.functionCallByInvoke | 500 | LinkedList | avgt | 10 | 1.577 | ± 0.030 | us/op |
ScopeBenchmark.functionCallByInvoke | 1000 | ArrayList | avgt | 10 | 3.137 | ± 0.028 | us/op |
ScopeBenchmark.functionCallByInvoke | 1000 | LinkedList | avgt | 10 | 3.147 | ± 0.052 | us/op |
ScopeBenchmark.functionCallByInvoke | 2000 | ArrayList | avgt | 10 | 6.301 | ± 0.087 | us/op |
ScopeBenchmark.functionCallByInvoke | 2000 | LinkedList | avgt | 10 | 6.240 | ± 0.018 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 100 | ArrayList | avgt | 10 | 0.321 | ± 0.001 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 100 | LinkedList | avgt | 10 | 0.320 | ± 0.004 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 200 | ArrayList | avgt | 10 | 0.663 | ± 0.030 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 200 | LinkedList | avgt | 10 | 0.642 | ± 0.006 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 500 | ArrayList | avgt | 10 | 1.609 | ± 0.070 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 500 | LinkedList | avgt | 10 | 1.592 | ± 0.027 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 1000 | ArrayList | avgt | 10 | 3.160 | ± 0.029 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 1000 | LinkedList | avgt | 10 | 3.160 | ± 0.036 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 2000 | ArrayList | avgt | 10 | 6.313 | ± 0.046 | us/op |
ScopeBenchmark.functionCallByScopeFunction | 2000 | LinkedList | avgt | 10 | 6.323 | ± 0.057 | us/op |
可以看出两种调用方式基本上没啥区别。