空安全处理
在Kotlin中, 最出名的特性莫过于就是它的空安全
了, 毕竟NPE
应该是大家最不想看到的错误信息.
我们先回顾下Kotlin如何处理空安全
我们有四种方法来避免NPE
- 在条件中检查null
- 安全调用使用?.
- 使用Elvis 操作符 ?:
- 使用!!操作符
当然关于第四点使用!!
操作符, 他的本质就回归到了当遇到null的时候仍然会抛出NPE. 所以在非必须的情况下, 我们应该尽量避免使用!!
为了避免NPE, 在kotlin的类型系统中, 它做到的就是强制开发者明确定义目标类型是否是可空类型(通过?
区别), 如果一个变量是可空的, 我们需要这样写
1 | var nullpossible: String? = null |
而像下面这种, 是永远不会编译通过的
1 | var nullpossible: String = null |
当我们在定义一个变量的时候, 当能够确保他是非空类型的时候就必须要在构造器中初始化, 然而这在实际开发中是非常不方便的.
1 | var a: String = ... |
我们可以利用几种方式来解决
- 使用
lateinit
延迟初始化 - 使用委托
Delegates.notNull()
1 | var test: String by Delegates.notNull() |
不管我们使用哪种方式, 都可以让我们避免在初次定义类型的时候就必须初始化工作, 当然不论哪种方式, 在初始化前调用属性都是会抛出异常的.
而关于Delegates.notNull()
, 通过源码我们可以看到他实际返回的是NotNullVar
的委托.而通过NotNullVar
中的getValue()
返回直接定义为非空属性.
1 | public object Delegates { |
还有一种方式是通过委托属性by lazy
, 但是他只可以修饰val
, 会在第一次调用对应属性的时候进行初始化, 默认是线程安全
1 | val testlazy: String by lazy { "fff"} |
另外他可以通过传入参数来选择不同的多线程处理
1 | .jvm.JvmVersion |
单例模式的实现
Kotlin提供了object
来很方便的支持了单例模式的实现
1 | object Singleton { |
我们看下他转为Java代码后是如何实现的.
1 | public final class Singleton { |
很好, 一个典型的饿汉式. 饿汉式的缺点我们简明讲下, 由于是类加载的第一时间就会新建实例, 所以当我们整个工程没有用到的时候, 就会导致内存空间的浪费.另外, 它无法自定义构造函数.
如果我们不适用object
呢, 应该如何实现单例模式?
我们来尝试用kotlin写一个DSL
单例模式, 先看java的实现方法
1 | public class SingletonDSL { |
ok, 下面是翻译工作
1 | class SingleDSLKotlin private constructor (){ |
根据上面lazy
的延迟初始化的特性(通过查看源码我们可以发现他内部也是用双重锁机制来实现的), 我们还可以更加的简单实现
1 | class SingleDSLKotlin private constructor (){ |
当然我们也可以通过静态内部类来实现单例模式
1 | class SingletonStaticClass private constructor(){ |
这里关于构造函数的相关基础知识可参见官网
域函数的区别
我们在前面写DSL单例的demo的时候, 用到了一个also
.我们开发过程中会经常用到这几个作用域函数run
, with
, apply
, with
, also
, let
要理解源码, 我们首先要搞明白inline
内联函数是做什么用的.
在kotlin中, 函数也是作为一个对象存储在内存中.当我们调用一个函数的时候, VM首先去找你函数存储的位置, 然后执行函数, 最后再回到你调用函数的地方. 这会分别引入了内存空间的开销和虚拟调用运行的时间开销
而内联函数做的就是在编译期就将函数的调用
替换成函数的定义
.
然后我们再回头看这几个函数的作用
let
我们最开始接触的作用域函数应该就是let
了, 当我们处理一个可空对象的时候, 要获取它的内部某个属性的时候, 我们一般都是通过使用?.let{}
来忽略掉空对象逻辑处理情况
1 | .internal.InlineOnly |
可以看到他是将自身T作为参数传入调用函数中, 然后返回最后执行的结果.
1 | private fun descriLet(){ |
run
1 | /** |
可以看出当我们使用T.run
的时候, 是作为T.()扩展函数的调用块, 最后返回闭包执行的结果
1 | private fun describeRun(){ |
also
1 | /** |
also
和let
有点像, 但是他返回的对象与闭包执行结果没有关系, 返回的是调用对象本身
1 | private fun describeAlso(){ |
apply
1 | /** |
作为T.()扩展函数调用块执行, 返回被调用对象本身
1 | private fun describeApply(){ |
with
1 | /** |
with
需要我们传入一个参数receiver
, 然后作为它的扩展函数执行闭包, 返回执行结果.
1 | private fun describeWith(){ |
关于他们在用处上的一些区别, 可以看这里