kotlin 深入学习笔记(一)
本文主要介绍kotlin的特性如下:
- nullsafety
- primitive is Object too
- first-class method
- 扩展函数、属性
在手里拿着铁锤的人看来,世界就像一颗钉子。
多学一门语言, 可以开拓眼界,促使我们换一个角度去看问题。
kotlin汲取了许多优秀的语言的设计经验,上手时满满的熟悉感,让kotlin几乎没有什么学习曲线。许多编程语言都是从实现业务逻辑、解决算法问题的角度去设计的,而kotlin考虑了程序员使用体验,站在了使用者的角度——一门好的编程语言,不应该让使用者去帮它解决设计缺陷,而应该致力于提高使用者的效率。
举个例子,用Java实现读asset目录下的一个文件:
public class Utils { |
上面一坨代码中,真正有用的代码只有两行。
public class Utils { |
使用kotlin实现一样的代码:
fun AssetManager.fileToString(filename: String): String { |
这就是代码的表现力。我想,除非公司根据代码行数给你支付工资,没人会想去写臃肿的Java代码版本。
本文是学习kotlin过程中,产生的一些笔记。
1. nullsafety
NPE 绝对是编程bug界的老大哥,毕竟上百万dollar的身价。
Java 也提供了@Nullable
和@NonNull
的Lint检查,不过对程序员而言,这只是可选项,而且即使在所有的代码中加上了这两个注解,仅仅是会在IDE中产生一条warning。
public boolean isEmpty(@Nullable List list) { |
而 kotlin 把 null 当做了一种新的类型,从而可以用类似类型检查的方式,把NPE扼杀在摇篮中。
nullsafety相关的基础语法:
- Type?
+ var a: Int? = null |
- 安全访问操作符
?.
这个二元操作符的意思是,在操作符左边的值不为null的情况下,访问右边的表达式并返回其值,否则返回null。
val a: Int? = null |
- Elvis operator (
?:
)
猫王 Elvis Presley (这脑洞不得不服) ?:
区别于Java中的三元操作符,这是个二元操作符。
如果左值非空,就返回左值,否则返回右值。
val a:Int? = null |
也可以在右边写 return、throw 语句,
val mStr = a?.toString() ?: return false |
- 强制cast 为非null类型
!!
val a : Int ? = null |
编译期不会检查null类型了。但是在运行期一样会抛出异常,所以这种写法是治标不治本。
在Android studio中使用自动转化功能生成的kotlin代码,到处都是这种!!
去掉!!
可以用以下几个策略:
1.使用 val 代替 var
强迫自己尽可能的使用不可变量。如果是可变变量,它在运行时有可能被赋值为null。
2.使用 lateinit
Android中许多变量需要在onCreate中初始化,这时可以使用 lateinit 修饰属性。
访问未初始化的变量将抛出UninitializedPropertyAccessException
。
private lateinit var mAdapter: RecyclerAdapter<Transaction> |
对于基础数据类型Int、Double,lateinit 不能使用,可以用delegates
private var mNumber: Int by Delegates.notNull<Int>() |
3.使用 let
function
let是kotlin std中自带的几个顶层函数,经常和安全操作符?.
配合使用。
private var mPhotoUrl: String? = null |
4.自定义 let
function
if (mUserName != null && mPhotoUrl != null) { |
可以模仿let写一个函数ifNotNull,这个函数接收带有两个参数的lambda类型。
fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) { |
调用如下
ifNotNull(mUserName, mPhotoUrl) { |
5.使用 Elvis operator
fun getUserName(): String { |
变成
fun getUserName(): String { |
6.just crash
有时候 crash 也是必需的,但是除了直接抛KotlinNullPointerException
,还可以用requireNotNull
或者 checkNotNull
封装异常信息。比如:
uploadPhoto(intent.getStringExtra("PHOTO_URL")!!) |
去掉!!
, 把Crash Exception封装下:
uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { "Activity parameter 'PHOTO_URL' is missing" })) |
查看kotlin编译出来的bytecode 就可以知道,这个 null check的特性是付出了代价的。在每次调用有非空参数的函数时、或者返回非空值的return语句前,都插入了下面一条类似这样的命令Intrinsics.checkXXXXIsNotNull
,自动帮我们check了一遍非空。
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V |
2. 没有primitive 数据类型
Java 认为万物都是对象,并创建了 Object 这个顶层类,但是primitive数据类型单独处理。
“凡动物一律平等
但是有些动物比别的动物更加平等”
——From: 乔治·奥威尔(George Orwell). 《动物农场》.
- primitive 存储在local stack空间,Object存储在 heap 空间
- primitive 作为参数是传值,Object作为参数是传引用
为了打通 primitive 数据类型和 Object,Java使用了一个包装类型作为bridge,使得这两个类型之间可以自动转化。
kotlin 的类型系统直接舍弃了 primitive 数据类型,真正意义上的万物都是对象。
在kotlin中可以直接在数据类型上调用、定义方法。
println(1498621482L.toDateString()) |
除此之外 Int 到 Long 到 Double的转化必须显式调用toXXXX()
。
kotlin的数据类型底层仍然是primitive数据:
val a = 1 |
反编译为Bytecode,再翻译为Java如下
private final int a = 1; |
3. first-class method
在kotlin中,可以把函数作为参数、返回值,也就是说,我们可以在kotlin中写高阶函数了。为了实现这个特性,kotlin提出了函数类型 () -> Type
,类似OC中的block。举例说明
fun trace(sectionName: String, body: () -> Unit){ |
这里的body
就是一个函数类型参数。
另外kotlin可以像Java8中的一样将lambda表达式赋值给java版的SAM-interface,或者赋值给kotlin的函数类型变量。
还是上面的例子,调用trace方法
trace("render",{ |
当lambda是最后(或唯一)一个参数时,可以把lambda大括号移到括号外面来。
trace("render") { |
这样简化有两个好处
- 减少括号的层级。
- 可以用kotlin直接写出类似DSL的声明式代码。例如,封装下okhttp,调用get时可以这样写:
get {
onSuccess = ...
onError = ...
header = ...
}
4. 扩展函数、属性
OCP
Software entities should be open for extension,but closed for modification
对扩展是开放的,对修改是关闭的
大多数Java项目,一定有一个或多个Utils类,里面一堆静态方法。当不确定某个helper方法写到哪儿时,new 一个Utils类太有诱惑力了。大家理直气壮地说,JDK也是这么干的,你看java.utils.**包下的都是这样写的啊。
Utils类为什么不好:
- 不符合OOP
- 不符合SOLID中的OCP、SRP原则
- 静态方法对单元测试不友好
不写Utils类,不用继承和组合,静态语言还能能像动态语言那样动态的扩展库函数吗?能。
在Objective-C中可以通过category给一个类增加方法,甚至覆盖方法。在kotlin中的扩展和OC的类似,但又有点区别。
- 扩展属性
public var TextView.text: CharSequence |
- 扩展函数
fun Long.toDateString(dateFormat: Int = DateFormat.MEDIUM): String { |
调用代码
println(1498621482L.toDateString()) |
不像OC修改了meta类的方法list,kotlin扩展特性的实现原理其实很简单。
上面的toDateString
“反编译”为Java代码后
public final class ExtentionUtilsKt { |
其实和我们自己写Utils静态方法一模一样。可以看出,kotlin的扩展属性就是语法糖。
当然,这种实现可以理解的,毕竟Java本身都没法舍弃Utils。kotlin没有重新实现一个Java,它只是在bytecode的基础上,提供了新的语法糖。
在了解实现细节后,显然,下面两个关于动态扩展的局限就很好理解了:
- 当扩展函数和成员函数有相同的函数签名时(override),成员函数优先于扩展函数。
- 扩展函数是静态分发的,总是使用函数的声明类型来决定调用哪个扩展函数
第一条,我们没有修改扩展类receiver的类结构,在调用同签名函数时,编译期会先查找receiver自己的方法表,只有没找到时,才会调用成Utils的静态方法。
第二条,看下面例子
open class C |
输出c,为什么不是d,这里不是子类型多态吗,难道多态在kotlin中不生效了?
验证一下,复写toString
方法
open class C { |
println(c)
输出了正确的结果。
Java的多态是采用方法动态绑定的实现,在编译后,toString
方法调用被编码成通过invokevirtual
指令调用,invokevirtual
会在运行时,去方法表里找对应的方法引用,而此时c
中toString
指向的是 D.toString 的实现,所以输出D不奇怪。
同样可以解释为什么foo
输出的却是c
。kotlin中的扩展函数,最终还是变成了某个Utils类中的静态方法,而静态方法的调用是静态绑定的,具体实现是使用invokestatic
指令,在编译期,该指令就知道调用方法的引用情况了,具体看下面的Java代码。
|
5. to be continued…
Author: deskid
Link: https://deskid.github.io/2017/06/29/kotlin-learn-notes-1/
License: 知识共享署名-非商业性使用 4.0 国际许可协议