干货 | Kotlin超棒的语言特性
作者简介
何伦,携程度假BU移动端资深研发经理,负责iOS、Android平台上跟团游产品预订流程的前端页面的研发工作。对新技术有着浓厚的兴趣。
自从2017年Google宣布Kotlin成为Android官方开发语言之后,Kotlin受到广大Android开发者的追捧。其强大的安全性,简洁性和与Java的互操作性,为开发者带来了耳目一新的开发体验,也极大提升了Android原生代码的开发效率。
不过大部分开发者对Kotlin的使用,仍然局限于把Java代码逻辑按照Kotlin语法进行转换的层面,其实Kotlin和Java虽然具有很强的互操作性,但本质上还是两种完全不同设计思想的语言。
本文在假定读者有一定Kotlin开发基础的前提下,详细讲解一些具有Kotlin特色的实用的语言特性,帮助开发者能够写出更加“具有Kotlin风格”的代码。这些语言特性包括空安全、Elvis表达式、简洁字符串等等。
01
更加安全的指针操作
在Kotlin中,一切皆是对象。不存在int, double等关键字,只存在Int, Double等类。
所有的对象都通过一个指针所持有,而指针只有两种类型:var 表示指针可变,val表示指针不可变。为了获得更好的空安全,Kotlin中所有的对象都明确指明可空或者非空属性,即这个对象是否可能为null。
对于可空类型的对象,直接调用其方法,在编译阶段就会报错。这样就杜绝了空指针异常NullPointerException的可能性。
如上图,编译器会报错
Error:Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String
02
?表达式和Elvis表达式
Kotlin特有的?表达式和Elvis表达式可以在确保安全的情况下,写出更加简洁的代码。比如我们在Android页面开发中常见的删除子控件操作,用Java来写是这样的:
为了获得更加安全的代码,我们不得不加上很多if else 判断语句,来确保不会产生空指针异常。但Kotlin的?操作符可以非常简洁地实现上述逻辑:
那么这个?表达式的内在逻辑是什么呢?以上述代码为例,若view == null,则后续调用均不会走到,整个表达式直接返回null,也不会抛出异常。也就是说,?表达式中,只要某个操作对象为null,则整个表达式直接返回null。
除了?表达式,Kotlin还有个大杀器叫Elvis表达式,即?: 表达式,这两个表达式加在一起可以以超简洁的形式表述一个复杂逻辑。
以上面表达式为例,我们以红线把它划分成两个部分。若前面部分为null,则整个表达式返回值等于c的值,否则等于前面部分的值。把它翻译成Java代码,是这样的
同样等同于这样
即Elvis表达式的含义在于为整个 ?表达式托底,即若整个表达式已经为null的情况下,Elvis表达式能够让这个表达式有个自定义的默认值。这样进一步保证了空安全,同时代码也不失简洁性。
03
更简洁的字符串
同Java一样,Kotlin也可以用字面量对字符串对象进行初始化,但Kotlin有个特别的地方是使用了三引号”””来方便长篇字符串的书写。而且这种方法还不需要使用转义符。做到了字符串的所见即所得。
同时,Kotlin还引入了字符串模板,可以在字符串中直接访问变量和使用表达式:
04
强大的when语句
Kotlin中没有switch操作符,而是使用when语句来替代。同样的,when 将它的参数和所有的分支条件顺序比较,直到某个分支满足条件。如果其他分支都不满足条件将会进入 else 分支。
但功能上when语句要强大得多。首先第一点是,我们可以用任意表达式(而不只是常量)作为分支条件,这点switch就做不到。如下述代码,前面三个分支条件分别是:1、变量在[1, 10]区间内, 2、变量x不在[10, 20]区间内,3、变量x是一个字符串。这个表达式用switch语句基本无法实现,只能用if else 链来实现。
说起if else 链,我们可以直接用when语句把它给替换掉:
05
对象比较
Java的 == 操作符是比较引用值,但Kotlin 的 == 操作符是比较内容, === 才是比较引用值。基于这点,我们可以写出逻辑更简洁合理的代码:
上述代码可以直接用when语句实现
06
Nullable Receiver
NullableReceiver我将其翻译成“可空接收者”,要理解接收者这个概念,我们先了解一下Kotlin中一个重要特性:扩展。Kotlin能够扩展一个类的新功能,这个扩展是无痕的,即我们无需继承该类或使用像装饰者的设计模式,同时这个扩展对使用者来说也是透明的,即使用者在使用该类扩展功能时,就像使用这个类自身的功能一样的。
声明一个扩展函数,我们需要用一个接收者类型,也就是被扩展的类型来作为他的前缀,以下述代码为例:
上述代码为 MutableList<Int> 添加一个swap 函数, 我们可以对任意 MutableList<Int> 调用该函数了:
其中MutableList<Int>就是这个扩展函数的接收者。值得注意的是,Kotlin允许这个接收者为null,这样我们可以写出一些在Java里面看似不可思议的代码。比如我们要把一个对象转换成字符串,在Kotlin中可以直接这么写:
上述代码先定义了一个空指针对象,然后调用toString方法,会不会Crash?其实不会发生Crash,答案就在“可空接收者”,也就是Nullable Receiver,我们可以看下这个扩展函数的定义:
扩展函数是可以拿到接收者对象的指针的,即this指针。从这个方法的定义我们可以看到,这个方法是对Any类进行扩展,而接收者类型后面加了个?号,所以准确来说,是对Any?类进行扩展。我们看到,扩展函数一开始就对接收者进行判空,若为null,则直接返回 “null” 字符串。所以无论对于什么对象,调用toString方法不会发生Crash.
07
关键字object
前面说过,Kotlin中一切皆为对象,object在Kotlin中是一个关键字,笼统来说是代表“对象”,在不同场景中有不同用法。
第一个是对象表达式,可以直接创建一个继承自某个(或某些)类型的匿名类的对象,而无须先创建这个对象的类。这一点跟Java是类似的:
第二,对象字面量。这个特性将数字字面量,字符串字面量扩展到一般性对象中了。对应的场景是如果我们只需要“一个对象而已”,并不需要特殊超类型。典型的场景是在某些地方,比如函数内部,我们需要零碎地使用一些一次性的对象时,非常有用。
第三,对象声明。这个特性类似于Java中的单例模式,但我们不需要写单例模式的样板代码即可以实现。
请注意上述代码是声明了一个对象,而不是类,而我们想要使用这个对象,直接引用其名称即可:
08
有趣的冒号
从语法上来看,Kotlin大量使用了冒号(:)这一符号,我们可以总结一下,这个冒号在Kotlin中究竟代表什么。
考虑下面四种场景:
在变量定义中,代表变量的类型
在类定义中,代表基类的类型
在函数定义中,代表函数返回值的类型
在匿名对象中,代表对象的类型
笼统来说,Kotlin的设计者应该就是想用冒号来笼统表示类型这一概念。
09
可观察属性
可观察属性,本质就是观察者模式,在Java中也可以实现这个设计模式,但Kotlin实现观察者模式不需要样板代码。在谈Kotlin的可观察属性前,先看下Kotlin里面的委托。同样的,委托也是一种设计模式,它的结构如下图所示:
Kotlin在语言级别支持它,不需要任何样板代码。Kotlin可以使用by关键字把子类的所有公有成员都委托给指定对象来实现基类的接口:
上述代码中,Base是一个接口,BaseImpl是它的一个实现类,通过by b语句就可以把Derived类中的所有公有成员全部委托给b对象来实现。我们在创建Derived类时,在构造器中直接传入一个BaseImpl的实例,那么调用Derived的方法等同于调用BaseImpl的实例的方法,访问Derived的属性也等同于访问BaseImpl的实例的属性。
回到可观察属性这个概念,Kotlin通过 Delegates.observable()实现可观察属性:
上述代码中,name是一个属性,改变它的值都会自动回调{ kProperty, oldName, newName -> }这个lambda表达式。简单来说,我们可以监听name这个属性的变化。
可观察属性有什么用处呢?ListView中有一个经典的Crash:在数据长度与Adapter中的Cell的长度不一致时,会报IllegalStateException异常。这个异常的根本原因是修改了数据之后,没有调用notifyDataSetChanged,导致ListView没有及时刷新。如果我们把数据做成可观察属性,在观察回调方法中直接刷新ListView,可以杜绝这个问题。
10
函数类型
Kotlin中一切皆是对象,函数也不例外。在Kotlin中,函数本身也是对象,可以拥有类型并实例化。Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明,比如我们常见的点击回调函数:
箭头表示法是右结合的,(Int) -> (Int) -> Unit 等价于(Int) ->((Int) -> Unit),但不等于 ((Int) -> (Int)) -> Unit。可以通过使用类型别名给函数类型起一个别称:
函数对象最大的作用是可以轻易地实现回调,而不需要像Java那样通过代理类才可以做到。我们以ScrollView滑动的回调为例,看一下使用Java编写一份Callback需要花费多大成本。对于主调方,即MyScrollView类而言,首先我们需要一个Callback的接口(OnScrollCallback),这个接口里面有一个待实现的onScroll方法。然后需要一个属性来保存回调对象。最后在View滑动的时候,我们调用这个回调对象的onScroll以实现回调。
对于被调方,即MyScrollView的使用者而言,我们需要一个实现OnScrollCallback接口的对象。然后设置成MyScrollView的回调对象,才能够实现滑动回调。
我们只是实现一个简单的回调而已,为什么还要这么复杂呢?本质上是因为Java里面函数并不是对象,所以要实现回调,必须要实现一个代理类来包装这个函数,否则我们无法传递这个函数给主调方。
Kotlin实现回调就是完全不一样的方式了,因为Kotlin的函数也是对象,所以我们直接把函数对象传递给主调方即可。
看一下上面的代码,就是这么简单!
再介绍下如何将函数类型实例化,有几种常见方式:
一是使用函数字面值的代码块,比如lambda 表达式 { a, b -> a + b },或者匿名函数fun(s: String): Int { return s.toIntOrNull()?: 0 }
二是使用已有声明的可调用引用,包括顶层、局部、成员、扩展函数 ::isOdd String::toInt,或者顶层、成员、扩展属性 List<Int>::size,或者是构造函数 ::Regex
三是使用实现函数类型接口的自定义类的实例
四是编译器推断
11
工具
对于初学Kotlin的开发者而言,编译器提供了贴心的小工具,甚至可以直接把Java代码转换成Kotlin代码。直接把Java代码拷贝到.kt文件中,编译器会弹出如下提示:
Kotlin与Java是100%兼容的,因为它最终会编译成Java字节码,我们可以通过 Android Studio工具看到编译的bytecode:
我们还可以把编译出来的Java字节码反编译成Java代码,这样可以窥探Kotlin的实现机理:
事实上,Kotlin优秀的语言特性绝对不止本文提到的这几种,还有很多,比如函数默认参数、扩展属性、懒初始化、局部函数、数据类,等等。欢迎大家在学习的过程中一起交流。
【推荐阅读】