那些年,我们看不懂的那些Kotlin标准函数

Kotlin标准库中提供了一套用于常用操作的函数。最近,在我的Kotlin交流群中有人再次问到了关于这些函数的用法。今天,让我们花一点时间,一起看一下这些函数的用法。

Ready go >>>

注:这里所说的标准函数主要来自于标准库中在文件Standard.kt中的所有函数。

run#1

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

contract部分主要用于编译器上下文推断,这里我们忽略掉这部分代码。

观察源码发现,run方法仅仅是执行传入的block表达式并返回执行结果而已(block是一个lambda表达式)。

因此,如果你仅仅需要执行一个代码块,可以使用该函数

看一个例子:

1
2
3
4
5
6
7
8
9
val x = run {
println("Hello, world")
return@run 1
}
println(x)

// 执行结果
Hello,world
1

run#2

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}

这个函数跟上面的函数功能是完全一样的。不同的是,block的receiver是当前调用对象,即在block中可以使用当前对象的上下文。

因此,如果你需要在执行的lambda表达式中使用当前对象的上下文的话,可以使用该函数。除此之外,两者没有任何差别

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}

class B {

}

fun main(args: Array<String>) {
val a = A()
val b = a.run {
// 这里你可以使用A的上下文
a.sayHi("Scott Smith")
return@run B()
}
println(b)
}

// 执行结果
Hello,Scott Smith
b@2314

从例子中,我们可以看到,这个函数还可以用于对数据类型进行转换。

with

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

这个函数其实和run函数也是做了一样的事情。不同的是,这里可以指定block的接收者。

因此,如果你在执行lambda表达式的时候,希望指定不同的接收者的话,可以使用该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
fun sayHi(name: String) {
println("Hello, $name")
}
}


fun main(args: Array<String>) {
val a = A()
with(a) {
// 这里的接收者是对象a,因此可以调用a实例的所有方法
sayHi("Scott Smith")
}
}

apply

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

可以看到,这个方法是针对泛型参数的扩展方法,即所有对象都将拥有该扩展方法。相对于run#2方法,apply不仅执行了block,同时还返回了receiver本身。

这在链式编程中很常用,如果你希望执行lambda表达式的同时而不破坏链式编程,可以使用该方法

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A {
fun sayHi(name: String) {
println("Hello, $name")
}

fun other() {
println("Other function...")
}
}


fun main(args: Array<String>) {
val a = A()
a.apply {
println("This is a block")
sayHi("Scott Smith")
}.other()
}

// 执行结果
This is a block
Hello, Scott Smith
Other function...

also

1
2
3
4
5
6
7
8
9
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

这个函数跟with又很像,不同的是,block带有一个当前receiver类型的参数。在block中,你可以使用该参数对当前实例进行操作。

这个函数和with完全可以互相通用,with函数可以直接在当前实例上下文中对其进行操作,而also函数要通过block参数获取当前类实例。因为用法完全一致,这里就不举例了

let

1
2
3
4
5
6
7
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}

如果你使用过RxJava,可能会感到似曾相识,这其实就是RxJava的map函数。这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展函数。

如果你希望对当前数据类型进行一定的转换,可以使用该方法。该方法的block中同样可以使用当前receiver的上下文

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Triangle {}

class Rectangle {}

fun main(args: Array<String>) {
val tr = Triangle()
val rect = tr.let { it ->
println("It is $it")
return@let Rectangle()
}
println(rect)
}

// 执行结果
It is Triangle@78308db1
Rectangle@27c170f0

从例子中可以看到,我们成功地将三角形转换成了矩形,这就是let函数的作用。

takeIf

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}

这个函数也是针对泛型参数的扩展函数,所有类都将拥有这个扩展。这个函数使用了一个预言函数作为参数,主要用于判断当前对象是否符合条件。
这个条件函数由你指定。如果条件符合,将返回当前对象。否则返回空值。

因此,如果你希望筛选集合中某个数据是否符合要求,可以使用这个函数

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeIf { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => null
2 % 2 == 0 => 2
3 % 2 == 0 => null

takeUnless

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (!predicate(this)) this else null
}

这个函数刚好与takeIf筛选逻辑恰好相反。即:如果符合条件返回null,不符合条件返回对象本身。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
val arr = listOf(1, 2, 3)
arr.forEach {
println("$it % 2 == 0 => ${it.takeUnless { it % 2 == 0 }}")
}
}

// 执行结果
1 % 2 == 0 => 1
2 % 2 == 0 => null
3 % 2 == 0 => 3

看到了吗?这里的执行结果和takeIf恰好相反。

repeat

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

这个函数意思很明显,就是将一个动作重复指定的次数。动作对应一个lambda表达式,表达式中持有一个参数表示当前正在执行的次数索引。

看一个例子:

1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
repeat(3) {
println("Just repeat, index: $it")
}
}

Just repeat, index: 0
Just repeat, index: 1
Just repeat, index: 2

简单总结

最后,我们用一个表格简单总结一下这些函数的用法:
函数|用途|特点|形式
:—:|:—:|:—:|:—:
run#1|执行block,并返回执行结果|block中无法获取接收者上下文|全局函数
run#2|执行block,并返回执行结果|block中可以获取接收者上下文|扩展函数
with|指定接收者,通过接收者执行block|block中可以获取接收者的上下文,可以对接收者数据类型做一定转换|全局函数
apply|执行block,并返回接收者实例本身|block中可以获取接收者的上下文,可用于链式编程|扩展
also|执行block,并返回接收者实例本身|block中有一个参数代表接收者实例,可用于链式编程|扩展
let|执行block,并返回执行结果|block中有一个参数代表接收者实例,可以对接收者数据类型做一定转换|扩展
takeIf|根据条件predicate判断当前实例是否符合要求|如果符合要求,返回当前实例本身;否则返回null|扩展函数
takeUnless|根据条件predicate判断当前实例是否不符合要求|如果不符合要求,返回当前实例本身;否则返回null|扩展

搞定Receiver

理解上面这几个函数,最重要的一点是要理解Receiver。遗憾的是,Kotlin官方文档中并没有针对Receiver的详细讲解。关于这部分的讲解,请扫描下方二维码关注欧阳锋工作室,回复搞定Receiver查看文章。

欢迎加入Kotlin交流群

关于Kotlin,如果你有任何问题,欢迎加入我的Kotlin交流群: 329673958。当前群交流活跃,问题解答速度很快,期待你的加入。

测测你的Kotlin基础

文 | 欧阳锋

本次测试满分160分,测测看,你能拿几分 <<<

1)Kotlin语言有基本数据类型吗?(5分)

2)Kotlin中有哪些访问控制符,分别代表什么意思?默认访问控制符是什么?(5分)

3)Kotlin接口是否允许有方法实现?是否允许声明成员变量?(5分)

4)Sealed类有什么作用?(5分)

5)Kotlin语言中如何实现类似Java创建匿名内部类对象?(10分)

6)Kotlin的扩展相对继承有什么优势?扩展方法的执行是否也遵循多态?(10分)

7)如果一个类同时实现多个接口,接口中存在同名方法,如何解决冲突?(5分)

8)Kotlin语言中是否存在static关键字,如果没有,如何声明静态变量,并实现与Java互通(5分)

9)使用Kotlin语言是否一定不会出现空指针异常?为什么?(10分)

10)Kotlin语言中推荐使用什么方式判断两个对象是否相等?如何判断两个对象是同一个对象?(5分)

11)如果使用Foo<out T: TUpper>这种方式声明泛型,使用Foo<*>这种方式接收该对象实例,代表什么意思?如何理解Kotlin泛型,与Java有什么区别?(10分)

12)如何自定义setter/getter方法?(5分)

13)使用语句var x = null声明变量x是否合法?如果合法,x的具体类型是什么?(5分)

14)下面这段代码的输出结果是什么?(10分)

1
2
3
val list = listOf(1, 2, 3)
list.add(4)
println(list)

15)下面这段代码的执行结果是什么?(5分)

1
2
3
4
5
6
7
8
9
// Kotlin端
object A {
fun init() {
println("A init")
}
}

// Java端
A.init()

16)下面代码的执行结果是什么?(5分)

1
2
3
fun sum(a: Int, b: Int) = { a + b }

println(sum(1, 3))

17)下面代码的执行结果是什么?(5分)

1
2
println(null is Any)
println(null!! is Nothing)

18)下面代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
init() {
f()
}

val a = "a"

fun f() {
println(a)
}
}

fun main(args: Array<String>) {
A()
}

19)下面代码的执行结果是什么?(10分)

1
2
println(127 as Int? === 127 as Int?)
println(128 as Int? === 128 as Int?)

20)下面代码的执行结果是什么?如果运行异常,应该怎样修改才能达到预期效果?(10分)

1
2
3
4
(1..5).forEach {
if (it == 3) break
println(it)
}

21)下面代码的执行结果是什么?如果运行异常,应该怎样修改,为什么要这样修改?(10分)

1
2
3
val A.x: Int = 3

println(A().x)

22)下面这段代码的执行结果是什么?(10分)

1
2
3
4
5
6
7
8
9
10
11
12
13
fun isOdd(x: Int) = x % 2 != 0

fun length(s: String) = s.length

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
return { x -> f(g(x)) }
}

fun main(args: Array<String>) {
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")
println(strings.filter(oddLength))
}

注:本篇例子Kotlin版本为1.2.31,更新版本可能存在部分差异

下面是你的基础等级:

得分 评价
0 ~ 80 基础较差
80 ~ 108 基础较好
108 ~ 160 基础很棒

查看答案方法

微信扫描下方二维码关注欧阳锋工作室,回复“Kotlin测试题答案”即可获取当前测试题答案

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

关于Kotlin抛弃可检测的异常处理,你怎么看?

可检测的异常英文翻译为Checked Exception,以下简称为CE。CE是一个备受争议的话题,有人主张CE是一个不可或缺的特性,也有人认为CE带来了一些问题,是一个冗余特性。这其中的支持者和反对者中都不乏软件行业的大佬。今天,我们借助这篇文章一起来讨论一下CE存在的必要性。

什么是CE

部分同学可能还不知道CE到底是什么。因此,在开始命题之前,有必要给大家解释一下CE的概念。

CE其实你每天都在用,只是你不知道它的存在而已。看一个例子你就明白了:

1
public void readString() throws IOException, FileNotFoundException {}

在这个函数声明的最后面,我们指定了函数可能抛出的异常,在使用的时候我们就可以针对具体的异常使用try {} catch() {}进行处理了。

由于我们在方法声明中指定了可能抛出的异常,因此方法具体可能抛出的异常是已知的,这就称之为可检测的异常(CE)。由于CE的声明来自编译阶段,因此IDE可能帮助你智能判断强制你针对某些异常进行处理,并给出友好提示。

这是一个很好的特性,不是吗?

想象一下,如果有一天,我们不能在函数上指定可抛出的异常了,会怎样?我们无法确定函数可能抛出的异常,并且可能会因为没有正确处理某个异常而导致程序奔溃。

可是,就是有人认为CE多此一举,并且Kotlin语言就是其中的支持者。为什么会有人坚定地认为CE多此一举呢?这是下一个我们要讨论的话题。

关于CE的争论

Java语言的CE设计借鉴了C++,而在受到Java影响的那些语言中,例如C#、Ruby等都去掉了CE的设计,这从实践的角度证明CE的存在确实意义不大。

在这个问题中,C#的主导工程师Anders Hejlsberg最有发言权。老实说,笔者并没有用过C#。可是,如果你搜索一下网络上关于C#和Java对比的文章你就会发现:C#被认为是一门比Java更优秀的编程语言,它始终在新增一些现代语言的特性,使你毫不费力地使用它。而Java作为一门古老的语言,受限于一些原始设计,在增加新特性时总是步履维艰,甚至有点不伦不类。

关于CE的设计,有人对Anders Hejlsberg进行过一次采访。采访的原文链接在这里:https://www.artima.com/intv/handcuffs.html

关于CE,Anders Hejlsberg认为它带来了两个问题版本问题扩展问题

所谓的版本问题是什么意思呢?Anders Hejlsberg举了一个例子:

假设有一个方法foo,它声明了抛出异常A、B和C,在下一个版本设计的时候,foo增加了一个新的特性,可能会抛出异常D。对于设计者来说,很明显这是一个大的改变,几乎可以确定的是,客户程序员不会去处理这个异常。为了避免出现问题,设计者不得不声明一个新的方法foo2,抛出一个新的异常。然后,客户程序员可以将针对foo的逻辑处理切换到foo2。

而所谓的扩展问题又是什么意思呢?这更好理解,以下来自Anders Hejlsberg的原话翻译并整理:

如果你在设计一个很小的系统,声明一个方法抛出一个异常,这很棒。可是,如果你尝试构建一个大的系统,其中包含了四、五个小系统的时候,问题来了。假设每个子系统可能抛出四到五个异常,而每上升一个系统,就犹如爬阶梯,异常数量会指数倍增加,最终你可能处理的异常将达到40个甚至80个。很显然,这是一个很糟糕的设计!

Anders Hejlsberg的话有理有据。可是,中国的 王垠 并不同意这个观点。关于Kotlin的CE设计,他写了一篇文章专门讲了这个问题,文章的原稿在这里:Kotlin 和 Checked Exception

看完王垠的文章,你会发现,他并不赞同Anders Hejlsberg的话。他认为,所谓的版本问题和扩展问题,其实都来自于程序员的滥用。只要处理得当,CE带来的好处是无法取代的。

其实,关于CE的争论还远不止Anders Hejlsberg和王垠两人。互联网上有很多关于这个问题的讨论。如果你感兴趣,可以Google了解一下。

你怎么看?

很显然,Kotlin语言受到了C#设计的影响,手起刀落,去掉了CE的设计。对于客户程序员来说,显而易见的一个改变是,你再也不能在Kotlin的方法声明中指定可能抛出的异常了。对于Kotlin语言的这种设计,你是赞成还是反对呢?

欢迎参与投票讨论

扫描下方二维码关注欧阳锋工作室,回复“CE”参与投票,或在所有文章中选择同名文章进行投票。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

你是否也被Kotlin语言的object绕晕了呢

文 | 欧阳锋

近日,在笔者的Kotlin语言交流群中。的确发现了一些同学对object的用法有一些疑问。于是,出现了下面这样错误的用法:

很自然的想法,c是一个接口类型的成员变量,访问外部类的成员变量,这不是理所应当的吗?

即使查看Kotlin官方文档,也有这样一段描述:

Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations.

核心意思是:Kotlin使用object代替Java匿名内部类实现。

很明显,即便如此,这里的访问应该也是合情合理的。从匿名内部类中访问成员变量在Java语言中是完全允许的。

这个问题很有意思,解答这个我们需要生成Java字节码,再反编译成Java看看具体生成的代码是什么。

借助JD-GUI,我们可以看到下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class Outer
{
private String a;

public static final class c
implements Moveable
{
public static final c INSTANCE;

static
{
c localc = new c();INSTANCE = localc;
}

public void move()
{
Moveable.DefaultImpls.move(this);
}
}
}

很有意思,我们在Kotlin类中object部分的代码最终变成了下面这个样子:

1
2
3
4
5
6
7
8
9
10
public static final class c implements Moveable {
public static final c INSTANCE;
static {
c localc = new c();INSTANCE = localc;
}

public void move() {
Moveable.DefaultImpls.move(this);
}
}

这是一个静态内部类,很明显,静态内部类是不能访问外部类成员变量的。可是问题来了,说好的匿名内部类呢?

这里一定要注意,如果你只是这样声明了一个object,Kotlin认为你是需要一个静态内部类。而如果你用一个变量去接收object表达式,Kotlin认为你需要一个匿名内部类对象。

因此,这个类应该这样改进:

1
2
3
4
5
6
7
8
9
10
11
12
class Outer {
private var a: String? = null

// 用变量c去接收object表达式
private val c = object: Moveable {
override fun move() {
super.move()
// 改进后,这里访问正常
println(a)
}
}
}

为了避免出现这个问题,谨记一个原则:如果object只是声明,它代表一个静态内部类。如果用变量接收object表达式,它代表一个匿名内部类对象。

object能干啥?

很自然地想到,Kotlin的object到底有什么作用。其实,从上文的表述来看。很明显,object至少有下面两个作用:

  • 简化生成静态内部类
  • 生成匿名内部类对象

其实,object还有一个非常重要的作用,就是生成单例对象。如果你需要在Kotlin语言中使用单例,非常简单,只需要使用object关键字即可。

1
2
3
4
5
6
7
8
9
object Singleton {
fun f1() {

}

fun f2() {

}
}

这种方式声明object和上面的方式略有区别,其最终会生成一个名为Singleton的类,并在类中生成一个静态代码块进行单例对象生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class Singleton
{
public static final Singleton INSTANCE;

public final void f1() {}

public final void f2() {}

static
{
Singleton localSingleton = new Singleton();INSTANCE = localSingleton;
}
}

在Kotlin语言中对方法进行访问的时候最终其实是通过INSTANCE实例进行中转的。

在Kotlin语言中还有一个很常用的object叫做伴随对象。所谓的伴随对象只不过是名字叫做Companion的object而已。它主要用于类中生成类似Java的静态变量,Kotlin语言针对这个变量会认为你只是希望生成一个静态变量,而不希望引入多余的类。如果你是和Java语言混合开发的话,可以使用一个注解生成和Java语言静态变量完全一样的效果。

简单总结

Kotlin语言中使用object命名的方式的确容易让人误认为只要使用这个关键字就是生成了一个对象。而从上文的表述当中,你会发现,其实不同的使用姿势将产生不同的效果。因此,在日常使用中一定要学会随机应变。如果遇到了不明白的问题,不妨来看看这篇文章是否已经解答了你的问题。如果没有,请在文章下方留言告诉我。

欢迎加入Kotlin交流群

如果你也喜欢Kotlin语言,欢迎加入我的Kotlin交流群: 329673958 ,一起来参与Kotlin语言的推广工作。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×