前言

本文并没有一条特别明确的主线,内容相对比较零星,会为咱们介绍一些Kotlin的常识,包含但不限于 语法特性、底层原理、奇巧淫技等,期望咱们能查漏补缺,更好的coding

工具

经过 Android Studio自带的Tools-> Kotlin -> Show Kotlin Bytecode

能够看到右侧会展现当时文件对应的JVM 字节码

点击 Decompile ,就会展现当时Kotlin文件反编译到Java代码后的成果

经过这种手法,能够了解到Kotlin编译器帮咱们做了哪些作业,更好的了解咱们的Kotlin代码

正文

根底语法

变量类型揣度的提示技巧

咱们都知道,在Kotlin中,咱们运用 var/val variableName:variableType = variableValue来声明变量,一起,由于Kotlin支持类型推导,所以能够简写为var/val variableName = variableValue

像下边这段代码,便是声明了一个变量,值是50.0

fun main(args: Array<String>) {
    val num = 50.0
}

作为有经历的研制,咱们都知道,浮点值默许是 double 类型,假如你需求一个float类型,你需求显式地在值后边加F,像这样

fun main(args: Array<String>) {
    val num = 50.0F
}

在这种比较简单的场景下,咱们能够简单看出Kotlin帮咱们揣度的类型,可是假如场景再复杂一些,咱们或许就会比较难进行肉眼的揣度,这个时分咱们有两种办法来帮咱们

经过快捷键 Control + Shift + P ,能够让IDE给咱们展现当时选中的Kotlin变量的类型

在IDE的设置中,咱们能够经过如下设置,让IDE总是显示当时的类型

翻开之后作用如下

除了咱们翻开的这一个,还有其他范围内的提示能够翻开,咱们能够自行尝试

Kotlin的包装类型

在Java中,根底类型分为原始类型和包装类型,比方intInteger,在Kotlin中,咱们只要Int这一种类型,可是这并不代表intInteger的差异就不存在了,仅仅Kotlin的编译器帮咱们去做了类型的挑选

关于下面的代码,左边是Kotlin的原始代码,右边是反编译后的Java代码

fun main(args: Array<String>) {
    val doubleNum = 1.0
    var doubleObjNum: Double? = 1.0
    doubleObjNum = null
    val list = ArrayList<Int>()
    list.add(1)
    val nullableList = ArrayList<Int?>()
    nullableList.add(1)
}

能够看到

  • 关于不行空的根底类型,Kotlin编译器会主动为咱们挑选运用原始类型,而关于可空的根底类型,Kotlin编译器则会挑选运用包装类型
  • 关于调集这种只能传包装类的状况,不管你是传可空仍是不行空,都会挑选运用包装类型

Kotlin中的数组

在Kotlin中,咱们运用arrayOf来创立一个数组,可是需求留意的是,运用arrayOf创立的数组都是包装类型的,而关于原始类型,需求运用intArrayOflongArrayOf等API来创立

这是一个比较简单被忽视的细节,需求留意一下,来防止不必要的功能损耗

fun main(args: Array<String>) {
    val intArray = intArrayOf()
    val integerArray = arrayOf<Int>()
}

函数的默许参数

咱们都知道,Kotlin的函数供给了默许参数这一特性,可是完成这一特性的原理是Kotlin在编译时,为咱们把默许参数在调用途填了进去,而不是真实完成了多个参数的办法

fun main(args: Array<String>) {
    foo()
}
fun foo(arg1: Int = 1, arg2: Long = 2) {
}

可是这么做在部分场景会带来一些古怪的问题

假如说你是一个SDK的作者,你的lib里供给了一个叫bar的空参办法,然后有其他的模块依靠了你的库,可是其他模块也是以AAR的方式被集成到APP里的,之后有一天,你需求给bar办法扩充功用,添加了一个参数

fun bar(arg: Boolean = false) {
}

之后你打了个版别,改了一下grade,打了个包,试了一下,发现用了这个办法的当地全都直接crash了

原因是由于,你添加了这个参数之后,这个办法的签名就变了,可是其他AAR又没有从头编译,不知道你的办法签名变了,所以运行时就全挂了

因而Kotlin为咱们供给了@JvmOverloads注解,经过这个注解,咱们能够生成多个办法来防止这一问题

fun main(args: Array<String>) {
    foo()
}
@JvmOverloads
fun foo(arg1: Int = 1, arg2: Long = 2) {
}

可是这种办法会导致包体积的胀大,一个有N个参数的办法,终究会生成2^N-1个办法

因而咱们需求灵活掌握,究竟什么时分运用@JvmOverloads注解

Run let apply also with的用法

runletapplyalsowith都是Kotlin官方为咱们供给的高阶函数

它们各自的receiverargumentreturn的差异能够看第一张图,咱们主要来看它们各自的适用场景

run适用于在顶层进行初始化时运用

let在被可空目标调用时,适用于做null值的查看

apply适用于做目标初始化之后的配置

also适用于与程序本身逻辑无关的副作用,比方说打印日志等

let在被非空目标调用时,适用于做目标的映射核算,比方说从一个目标获取信息,之后对另一个目标进行初始化和设置终究回来新的目标

Data Class

在Kotlin中,咱们能够在class关键词之前加一个data,来声明它为一个数据类

之后Kotlin在编译的时分,会主动为咱们生成copyequalstoStringhashCode还有componentN办法

其中componentN办法能够为咱们完成一种叫做「解构」的语法特性

data class Test(
    val name: String,
    val age: Int
)
fun main() {
    val test = Test("test", 1)
    val (name, age) = test
}

咱们来一同看一下data class反编译的成果

能够看到,解构语法实质上仍是在调componentN办法,N便是在结构办法里界说的第N个参数

一起咱们需求留意一下toString这个办法,它会输出你的变量名,由于toString是编译时生成的,所以即使是在release包上,它仍然会不受混杂的影响,把当时变量名进行输出,所以假如你的类含有一些比较灵敏的信息,请在运用前思考一下,会不会有安全风险

==和===

在Java中咱们一般运用==来判别两个目标的引证是否持平,运用equals办法来判别两个目标是否值持平

咱们或许会很天然的把这种主意也带入到Kotlin中,可是在Kotlin中,==equals是持平的,假如咱们要判别两个目标的引证是否持平,要运用===来判别

fun main() {
    val str1 = "a"
    val str2 = "b"
    println(str1 == str2)
    println(str1 === str2)
}

Lateinit 和 by lazy

Property initialization using “by lazy” vs. “lateinit”

【译】kotlin中lateinit和by lazy的差异 –

Kotlin中,咱们能够运用lateinit var或许by lazy来完成变量的延迟初始化

但两者的适用场景又不太相同

lateinit var适用于你在声明变量时不知道它的初始值是多少的场景

比方你要声明一个String类型的变量,可是它的初始值依靠后续流程去给它声明

这种状况下,咱们当然能够直接把它声明成var str:String? = null,之后再对它进行赋值

可是这样咱们后续在运用它的时分都需求对它进行判空操作,代码写起来就怪怪的

这个时分咱们就能够把它声明成lateinit var str:String,这样后续它都是一个非空类型了,不过就需求你自己确保代码访问时序的正确性,假如没有赋值就去访问了,会有反常被抛出

lazy更适用于,「一个目标的创立需求耗费许多的资源,而我不知道它究竟会不会被用到」的场景,lazy只要在第一次被调用到的时分才会去赋值

fun main() {
    val any by lazy {
        println("create any instance")
        Any()
    }
    println("before creation")
    val value = any
    println("after creation")
}
// before creation
// create any instance
// after creation

那么lazy是怎样完成懒加载的呢,lazy实质是生成了一个SynchronizedLazyImpl目标,这个目标初始化的时分会持有一个函数的引证,当调用它的value的时分,会去查看是不是初始化过了,假如初始化过了直接回来,没有的话调用传入的函数,获取到回来值之后再回来,然后完成了一个懒加载的作用

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
}

可是上面的代码中,咱们能够看到,这个完成它在第一次获取值的时分是有加锁来完成线程安全,可是许多时分咱们的代码都是单线程调用的,不需求考虑线程安全问题,这个时分就会有额定的功能开支

不过好在Kotlin还为咱们供给了其他的Lazy完成版别,比方运用CAS来完成线程安全的SafePublicationLazyImpl,以及没有任何线程安全保护措施的UnsafeLazyImpl

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

咱们能够在运用时传入mode来挑选运用哪一种完成办法

val value by lazy(LazyThreadSafetyMode.NONE) {}

当然也能够进一步封装

fun <T> lazyFast(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)

然后这么运用

val value by lazyFast {}

看咱们个人的偏好

可是在运用lazy的时分,咱们需求当心的运用外部的变量,由于有或许会形成内存泄露

咱们来看这个比方

class MainActivity {
    private val context: Any = Any()
    val foo by lazy {
        println(context)
        Any()
    }
}

运算符重载与中缀表达式

关于下面的代码,咱们在声明Map的时分,运用to来界说了键值对,之后又运用[]来访问元素,这在Java中是做不到的,那么Kotlin是怎样完成这一作用的呢

fun main() {
    val map = mapOf(
        1 to "one",
        2 to "two"
    )
    val value = map[1]
}

首先咱们来看to的完成,只要短短一行

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

能够看到to也仅仅一个办法,那咱们代码实质上是这样的,下面这段代码相同能够经过编译

val map = mapOf(
    1.to("one"),
    2.to("two")
)

而之所以咱们能够像上面那么写,是由于to办法中有一个infix关键字,这个关键字表示这个办法能够做一个「中缀表达式」,经过给办法添加这个关键字,咱们能够完成variableA functionName variableB这样的调用,来进步咱们代码的可读性

在了解了这一原理之后咱们能够认识到,这一特性不局限于Kotlin供给的类,咱们相同能够给自己的办法添加这一特性

fun main() {
    val test = Test()
    test foo ""
}
class Test {
    infix fun foo(any: Any) {
    }
}

之后咱们来看一下经过[]来访问Map的元素是怎样完成的

//Kotlin代码
val value = map[1]
//反编译之后的Java代码
String value = (String)map.get(1);

咱们能够看到这种办法实质上仍是在调Map的get办法,咱们来看一下get办法的声明

public operator fun get(key: K): V?

能够看到get办法多了一个operator关键字,Kotlin编译器正是经过这个关键字才把[]和get办法进行了映射,除了get之外Kotlin还供给了许多的operator供咱们运用,完好的能够在官网文档查看

Operator overloading | Kotlin

同理,作为一个关键字,咱们也能够为自己类覆写操作符办法来让调用者更便利地进行调用

比方下面这个比方,咱们经过覆写compareTo办法,能够让调用方直接用 >= 来判别两个目标的巨细

class Test {
    operator fun compareTo(any: Any):Int {
        return 0
    }
}
fun main() {
    val test = Test()
    val test2 = Test()
    println(test >= test2)
}

List与Sequence

Kotlin为咱们供给了许多的调集操作函数供咱们对调集进行操作,比方filtermap等,但这些函数并不是没有副作用的

来看这样一个比方,咱们界说了一个1~20的调集

然后经过两次调用filter函数,来先筛选出调集中的偶数,然后又筛选出调集中的5的倍数,终究得到成果10和20,看起来一切都很正常

fun main() {
    val list = (1..20).toList()
    val result = list.filter {
        print("$it ")
        it % 2 == 0
    }.also {
        println()
    }.filter {
        print("$it ")
        it % 5 == 0
    }
    println()
    println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
// 2 4 6 8 10 12 14 16 18 20 
// [10, 20]

可是咱们来看一下filter的源码

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

经过filter的源码咱们能够知道,每次filter操作都会创立一个新的调集目标,假如你的操作次数许多而且你的调集目标很大,那么就会有额定的功能开支

针对这种状况,Kotlin为咱们供给了Sequence优化功能

fun main() {
    val list = (1..20).toList()
    val sequenceResult = list.asSequence()
        .filter {
            print("$it ")
            it % 2 == 0
        }.filter {
            print("$it ")
            it % 5 == 0
        }
    val iterator = sequenceResult.iterator()
    iterator.forEach {
        print("result : $it ")
    }
}
// 1 2 2 3 4 4 5 6 6 7 8 8 9 10 10 result : 10 11 12 12 13 14 14 15 16 16 17 18 18 19 20 20 result : 20 

关于Sequence,它的核算是惰性的,在调用filter的时分,并不会当即核算,只要在调用它的iteratornext办法的时分才会进行核算,而且它并不会像List的filter一样核算完一个函数的成果之后才会去核算下一个函数的成果,而是关于一个元素,用它直接去走完一切的核算

举个比方

关于1,它走到第一个filter里边,不满足条件,直接就结束了

而关于2,它走到第一个filter里边,契合条件,这个时分会持续拿它去走第二个filter,不契合条件,就回来了

关于10,它走到第一个filter里边,契合条件,这个时分会持续拿它去走第二个filter,仍然契合条件,终究就被输出了出来

因而,假如你对调集的操作次数比较多的话,引荐运用Sequence来操作防止不必要的功能损耗

扩展办法

Kotlin供给了扩展办法和扩展特点,能够对一些咱们无法修正源码的类,添加一些额定的办法和特点

一个很简单的比方,String是JDK供给的类,咱们没有办法直接修正它的源码,可是咱们又常常会做一些判空、判别长度的操作,在以往运用Java的时分,咱们会运用TextUtils.isEmpty来判别,可是有了Kotlin之后,咱们能够像下面这样,给String界说一个扩展办法,之后在办法体中,运用this就能够办法到当时的String目标,然后完成看起来为这个类新增了一个办法的作用

fun main() {
    "".isEmpty()
}
fun String.isEmpty(): Boolean {
    return this.length > 0
}

但这仍然是Kotlin编译器的魔法,终究它在调用时,仍是以一个办法的方式调用的,所以扩展办法并没有真实的为这个类添加新的办法,而仅仅让你在写代码时能够像调用办法一样调用工具类,来添加代码的可读性

在了解这一原理之后,咱们就能够理解在一些特别case下,Kotlin的扩展为什么会体现很“古怪”

在下面这个比方里,类A已经有一个bar办法了,然后咱们又经过扩展办法给它界说了一个bar办法,它在语法上是可行的

class A {
    fun bar() = println("inner bar")
}
fun A.bar() = println("extend bar")
fun main() {
    val a = A()
    a.bar()
}

可是咱们在调用的时分,永久调到的是类A本身的bar办法,由于从底层来说,类A自己的办法和扩展办法,办法签名是不一样的,Kotlin编译器发现你本身有这个办法了,就不会再给你做扩展办法的调用

咱们来看另一个比方

咱们现在有一个类A,然后B承继了A,之后别离扩展了一个同名办法printSelf,A的输出A,B的输出B,之后咱们在main办法里进行如下调用

open class A {
}
class B : A() {
}
fun A.printSelf() = println("A")
fun B.printSelf() = println("B")
fun main() {
    val a = A()
    a.printSelf()
    val b: A = B()
    b.printSelf()
    val b2 = B()
    b2.printSelf()
}
// 输出成果是
// A
// A
// B

第一和第三个的输出成果都清楚明了,可是第二个的输出成果却是A,为什么会这样呢

由于咱们把b的类型声明成了A,尽管它是一个B的实例,但Kotlin编译器又不知道你运行时究竟是什么,你声明是A,就给你调用A的扩展办法,所以才会有这一成果

递归优化

在开发过程中,咱们或许会常常运用到递归,从语义上来说,递归更简单被程序员理解,可是也有一个丧命的缺陷,便是假如层级过深,会报StackOverFlow

在Kotlin中,Kotlin为咱们供给了tailrec关键字来做尾递归优化,关于下面这个用来求阶乘的函数,它编译之后成果,不再是一个递归调用,而是一个while循环,这样它就一起具备了很好的可读性和防止了层级过深到来的爆栈问题

fun main() {
    println(factorial(5))
}
tailrec fun factorial(n: Int, run: Int = 1): Long {
    return if (n == 1) run.toLong() else factorial(n-1, run*n)
}

但并不是一切的递归函数都能够运用tailrec关键字来做优化,只要尾递归函数能够这么做,所谓尾递归,是指「对自身的函数调用是它执行的终究一个操作」,只要这样的递归函数,Kotlin的编译器才能为咱们做剖析和优化

Unit不是void

在Kotlin中,假如一个办法没有声明回来类型,那么它的回来类型会被默许设置为Unit

可是Unit并不等同于Java中的void关键字,void代表没有回来值,而Unit是有回来值的

fun main() {
    val foo = foo()
    println(foo.javaClass)
}
fun foo() {
}
// 输出成果:class kotlin.Unit

从上面这个代码咱们能够看出来,Unit的办法回来了一个Unit的目标,咱们来看一下Unit是什么

public object Unit {
    override fun toString() = "kotlin.Unit"
}

很简单理解,Unit便是一个全局唯一的单例目标,那么Kotlin这么做的含义是什么呢

答案是为了抹除void的函数的特别性,void是真实含义上的「什么都不回来」,可是Unit函数是回来了值的,仅仅这个值在咱们平时调用时并不太去关怀,这样一来,在Kotlin里,一切的函数就都是有回来值的,这样才能更便利地让Kotlin对函数去做笼统,以完成更多Kotlin的高级特性

不强制的check exception

Kotlin对有反常抛出的办法,在调用途并不强制要求进行try catch 操作

下面这段代码,相同一个办法,在Java中调用,编译器会强制要求你进行try catch,但Kotlin不会

由于在Kotlin设计者的视点来看,有许多Exception只要在很极点的case下才会呈现,关于这种状况咱们没有必要为了极点case而去在调用的每一处当地都加try catchblock然后使咱们的代码处处都是try catch,Kotlin挑选把这项权利交给用户,假如你觉得这块代码或许会有问题,你能够自行加上try catch,而不是强制要求每处调用都加。

因而咱们在调一些办法的时分,最好仍是仔细思考一下,这部分是否需求try catch,然后防止不必要crash

高级特性

高阶函数

所谓高阶函数,是指在Kotlin中,函数也能够作为另一个函数的入参或许回来值

fun foo(function: () -> Unit) {
    function()
}
fun bar(): (() -> Unit) {
    return {  }
}
fun main() {
    foo {
        println("foo")
    }
}

可是咱们都知道,JVM本身是没有函数类型的,那Kotlin是怎样完成这种作用的呢

经过反编译,咱们能够看到,终究foo办法传入的类型是一个Function0类型,然后调用了Function0的invoke办法,那么这个Function0是啥呢,咱们看下它的界说

public interface Function0<out R> : Function<R> {
    /** Invokes the function. */
    public operator fun invoke(): R
}

这个类型有一个invoke办法,便是咱们办法的办法体

也便是说,下面这段Kotlin代码

fun main() {
    foo {
        println("foo")
    }
}

等价于这段Java代码

public static void main(String[] args) {
    foo(new Function0<Unit>() {
        @Override
        public Unit invoke() {
            System.out.println("foo");
            return Unit.INSTANCE;
        }
    });
}

所以Kotlin的高阶函数实质上是经过对函数的笼统,然后在运行时经过创立Function目标来完成的。

其中Funtion0代表0个参数的函数类型,那同理的,还有Function1Function2,一直到Function22

那这是否是说,Kotlin的高阶函数的参数的函数最多有22个参数呢?

1.3之前是的,可是从1.3开始,Kotlin供给了FunctionN来应对22个参数以上的场景

@SinceKotlin("1.3")
interface FunctionN<out R> : Function<R>, FunctionBase<R> {
    /**
     * Invokes the function with the specified arguments.
     *
     * Must **throw exception** if the length of passed [args] is not equal to the parameter count returned by [arity].
     *
     * @param args arguments to the function
     */
    operator fun invoke(vararg args: Any?): R
    /**
     * Returns the number of arguments that must be passed to this function.
     */
    override val arity: Int
}

自此,参数的长度不会再受限制

奇特的inline

在上一条中,咱们提到了,高阶函数在调用时总会创立新的Function目标,假如这个高阶函数会被频繁调用,那么就会有许多的目标被创立,那么就会有功能问题

为此,Kotlin为咱们供给了inline关键字,关于下面这段代码,咱们给foo函数加了inline关键字,之后咱们来看一下它的反编译的成果

inline fun foo(function: () -> Unit) {
    function()
}
fun main() {
    foo {
        println("foo")
    }
}

能够看到,main办法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用

这便是inline的作用,「内联」,经过inline,咱们能够把函数调用替换到实际的调用途,然后防止Function目标的创立,进一步防止功能损耗。

假如你供给的办法对功能要求很高,那么咱们引荐你运用inline关键字,可是请留意,inline是有副作用的,由于inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的当地,然后导致字节码的胀大,假如你的产物对产物巨细有严厉的要求,你需求自己掌握功能和包体积之间的取舍联系

reified帮你完成真泛型

众所周知,JVM的泛型是假泛型,尽管咱们有办法在运行时获取泛型的类型信息,可是像下面这种代码,是没有办法经过编译的,由于在编译时没办法经过泛型参数T来获取到详细的类型信息

fun <T> foo() {
    println(T::class.java) // 会报错
}

可是Kotlin为咱们供给了reified关键字,经过这个关键字,咱们就能够让上面的代码成功编译而且运行

inline fun <reified T> foo() {
    println(T::class.java)
}

reified关键字有必要和inline调配运用,上面说过了,inline会把函数体替换到调用途,调用途的泛型类型必定是确定的,那么就能够直接把泛型参数进行替换

比方说这行调用

foo<String>()

调用的泛型类型为String,那inline进行内联的时分,就能够直接把这一行代码替换为

println(String::class.java)

然后达到了「真泛型」的作用

SAM转化与fun interface

关于这样的Java代码

public interface OnClickListener {
    void onClick(Object view);
} 
public class Test {
    private OnClickListener onClickListener;
    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }
}

咱们在进行Kotlin的调用的时分,能够直接这么写

fun main() {
    val test = Test()
    test.setOnClickListener { view ->
        println("clicked")
    }
}

可是为什么呢,正常来说不是应该像下面这么写吗?

fun main() {
    val test = Test()
    test.setOnClickListener(object : OnClickListener {
        override fun onClick(view: Any?) {
            println("clicked")
        }
    })
}

咱们把上面的OnClickListener称为SAM(Single Abstract Method 只要一个办法的接口,称之为“单个笼统办法接口”),关于SAM,咱们其实并不是想要这个接口类型,而是想要接口中的那个函数,所以Kotlin的编译器帮咱们做了底层作业,让咱们在上层调用时,关于SAM类型,能够直接传一个和接口中函数类型一致的函数,像这样

fun main() {
    val test = Test()
    test.setOnClickListener({ view ->
        println("clicked")
    })
}

一起由于Kotlin的另一个语法特性,「假如函数是函数入参的终究一个参数,那么它能够被放到括号后,假如放到括号后没有其他参数,则括号能够省略」,终究完成了咱们最开始的那种作用

可是需求留意的是,这种SAM转换的特性,只对Java的接口生效,假如咱们把接口改为Kotlin

interface OnClickListener {
    fun onClick(view: Any?)
}

则编译就会报错,由于Kotlin编译器不会为Kotlin的SAM做转换,导致类型无法识别

可是这种特性属实是不太友爱,所以从Kotlin 1.4开始,支持了fun interface这一特性,咱们给Kotlin的接口声明上加一个fun关键字,来标识这是一个SAM的接口

fun interface OnClickListener {
    fun onClick(view: Any?)
}

这样Kotlin针对这个接口,也能够做SAM转化了

署理能帮咱们做什么

在Kotlin中,供给了by关键字来做署理

比方说关于以下代码,咱们给Bar的结构函数传入了一个Foo的目标,然后把详细的完成署理给了传入的Foo目标,可是这样写起来很麻烦,每新增一个办法都需求咱们手动去完成,而且还会导致这个类的行数迅速胀大。

interface Foo {
    fun foo()
}
class Bar(private val foo: Foo) : Foo {
    override fun foo() {
        foo.foo()
    }
}

可是有了by之后,咱们能够这样写

interface Foo {
    fun foo()
}
class Baz(private val foo: Foo) : Foo by foo {
}

现在一切的办法都被主动署理给了foo目标,不需求咱们再手动保护

by的作用还不止如此,它还能够署理特点。Kotlin供给了ReadOnlyPropertyReadWriteProperty来让咱们做只读和读写的特点的署理,咱们来看一个比方

class DelegateObject : ReadWriteProperty<Any?, String> {
    private var content = ""
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {
        content += Random.nextInt().toString()
        return content
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        content = value + value
    }
}
fun main() {
    var str by DelegateObject()
    println(str)
    str = "foo"
    println(str)
}
//-2143661939
//foofoo-1905752020

在这个比方中,咱们把str署理给了DelegateObject目标,那么这个str变量的读写都会被署理到对应的getValuesetValue办法上

在这个署理中,咱们让它每次赋值的时分,都会赋付过来的值再额定拼接一遍,在读值的时分会每次都在结尾加一个随机的Int值,这样就有了咱们代码的作用,第一次读值的时分尽管咱们什么值都没有赋,可是会有一个数字,第2次咱们尽管赋值的是foo,可是读值读出来又不一样

经过这种机制,咱们能够能够做到许多简洁的工具类的封装,比方说下面这个SharedPreference的署理

class Preference<T>(
    val context: Context,
    val name: String = "",
    val default: T,
    val prefName: String = "default"
)
    : ReadWriteProperty<Any?, T>{
    private val prefs by lazy {
        context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
    }
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(findProperName(property))
    }
    private fun findProperName(property: KProperty<*>) = if(name.isEmpty()) property.name else name
    private fun findPreference(key: String): T{
        return when(default){
            is Long -> prefs.getLong(key, default)
            is Int -> prefs.getInt(key, default)
            is Boolean -> prefs.getBoolean(key, default)
            is String -> prefs.getString(key, default)
            else -> throw IllegalArgumentException("Unsupported type.")
        } as T
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(findProperName(property), value)
    }
    private fun putPreference(key: String, value: T){
        with(prefs.edit()){
            when(value){
                is Long -> putLong(key, value)
                is Int -> putInt(key, value)
                is Boolean -> putBoolean(key, value)
                is String -> putString(key, value)
                else -> throw IllegalArgumentException("Unsupported type.")
            }
        }.apply()
    }
}

咱们在运用的时分,就能够这么运用

var userName by Preference(this, default = "")

这样咱们在读写这个值的时分,既能够拿到这个值,又能够直接把这个值和SharedPreference存储的值进行同步,非常便利

Contract的妙用

在开发的过程中,咱们或许用过Kotlin供给的函数,比方像下面的isNullOrEmpty,当咱们在运用它的时分,假如判别了它不是空,即使它的类型本身是可空的,可是编译器会帮咱们做揣度,进入到if分支里去后,str必定是不会为空的

可是当咱们自己写一个相似的函数的时分,咱们会发现,编译器并不会为咱们做类型的揣度

这是为什么呢?

咱们来看一下isNullOrEmpty这个办法的完成

public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }
    return this == null || this.length == 0
}

能够看到这个办法就仅仅return了null值和lenth的判别,只不过在return前调用了contract

contract意为「契约」或「约好」,这个办法正是经过contract和编译器达到了约好,在这个办法回来false的时分(returns(false)),当时的值必定不为空(implies (this@isNullOrEmpty != null)

知道了原理咱们能够依样画葫芦来改造咱们自己的函数

能够发现现在咱们自己的函数调用的当地,Kotlin编译器也会为咱们做类型的揣度了

除了做类型的揣度,契约还能够做许多风趣的事情,比方下面这个比方

为什么Kotlin自带的let能够完成对num的赋值,而咱们自己完成的myLet不行呢

由于Kotlin自己的let,运用契约告诉了Kotlin编译器,block这个函数只会被调用一次,所以num天然能够能够是val的,而咱们自己完成的myLet,编译器不知道block究竟会被调用几次,假如被调用屡次的话,num2有必要要是var的,所以这里才会报错

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

contract目前供给了一下几个办法,供咱们和编译器达到约好,经过这几个办法,咱们能够完成许多意想不到的作用,咱们能够开动脑筋发掘一下

public interface ContractBuilder {
    /**
     * Describes a situation when a function returns normally, without any exceptions thrown.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsContract
    @ContractsDsl public fun returns(): Returns
    /**
     * Describes a situation when a function returns normally with the specified return [value].
     *
     * The possible values of [value] are limited to `true`, `false` or `null`.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsTrueContract
    // @sample samples.contracts.returnsFalseContract
    // @sample samples.contracts.returnsNullContract
    @ContractsDsl public fun returns(value: Any?): Returns
    /**
     * Describes a situation when a function returns normally with any value that is not `null`.
     *
     * Use [SimpleEffect.implies] function to describe a conditional effect that happens in such case.
     *
     */
    // @sample samples.contracts.returnsNotNullContract
    @ContractsDsl public fun returnsNotNull(): ReturnsNotNull
    /**
     * Specifies that the function parameter [lambda] is invoked in place.
     *
     * This contract specifies that:
     * 1. the function [lambda] can only be invoked during the call of the owner function,
     *  and it won't be invoked after that owner function call is completed;
     * 2. _(optionally)_ the function [lambda] is invoked the amount of times specified by the [kind] parameter,
     *  see the [InvocationKind] enum for possible values.
     *
     * A function declaring the `callsInPlace` effect must be _inline_.
     *
     */
    /* @sample samples.contracts.callsInPlaceAtMostOnceContract
    * @sample samples.contracts.callsInPlaceAtLeastOnceContract
    * @sample samples.contracts.callsInPlaceExactlyOnceContract
    * @sample samples.contracts.callsInPlaceUnknownContract
    */
    @ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}