Kotlin读书笔记之优雅且高效的Kotlin

一. Kotlin的流畅性

说到运算符重载,是不是想起来大学被C++折磨的日子。传统上,我们对数值类型来创建表达式,例如:2 + 3 或 4.2 * 7.1。运算符重载是一种特性,是该语言对用户定义的数据类型扩展了使用运算符的能力。

但是由于JVM不支持运算符重载,Kotlin通过运算符映射到特殊的命名方法。使得运算符虫重载成为了可能。

下面先看一下Kotlin支持的重载映射:

运算符对应于观察值+xx.unaryPlus()-xx.unaryMinus()!xx.not()x+yx.plus(y)x-yx.minus(y)x*yx.times(y)x/yx.div(y)x%yx.rem(y)++xx.inc()x必须是可赋值的x++x.inc()x必须是可赋值的–xx.dec()x必须是可赋值的x–x.dec()x必须是可赋值的x==yx.equals(y)x!=y!(x.equals(y))x

接着看一个关于复数乘法的例子:

import kotlin.math.abs

data class Complex(val real: Int, val imaginary: Int) {

    operator fun times(other: Complex) =
        Complex(real * other.real - imaginary * other.imaginary,
            real * other.imaginary + imaginary * other.real)
    private fun sign() = if (imaginary < 0) "-" else "+"
    override fun toString() = "$real${sign()}${abs(imaginary)}i"
}

写好之后看一下两个复数相乘:

println(Complex(4, 2) * Complex(-3, 4))

运算符重载必须遵守一些规则:

  • 需要谨慎使用
  • 只有当用途对读者看起来很明显才重载
  • 遵守运算符的通常理解的行为
  • 对变量使用有意义的名称,这样更容易理解重载的上下文。

kotlin允许你将方法和属性注入任何类中,包括用其他JVM语言编写的类。在kotlin中对于类的扩展是开放的。

扩展函数和扩展属性是添加方法和属性的技术,但不会改变目标目标类的字节码。kotlin允许将方法和属性注入到现有的类中,包括final类。

假设在现有的程序中有Point和Circle两个类,定义如下:

data class Point(val x: Int, val y: Int)
data class Circle(val cx: Int, val cy: Int, val radius: Int)

假设现在想知道一个点是否是在圆上。在不改源码的基础上,我们使用扩展函数注入:

fun Circle.contains(point: Point) =
    (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) <
            radius * radius

在contains()扩展函数中,访问隐式Circle实例成员,如同实例方法一样访问,如下:

val circle = Circle(10, 10, 25)
val point = Point(5, 10)
println(circle.contains(point))

当涉及扩展函数时,方法调用实际上是对静态方法的调用。

当kotlin看到扩展函数时,它在扩展函数所在的包中创建一个静态方法,并将context对象(本例中的Circle)作为第一个参数传递给函数,而实际的参数作为其余的参数传递。当编译器看到对方法的调用时,它会认为我们正在调用扩展函数时,并将context对象circle作为第一个参数路由的调用个时,它会认为我们正在调用扩展函数,并将context对象circle作为第一个参数路由到该方法。

扩展函数有一些限制:

  • 当扩展函数和同名的实例方法之间发生冲突时,实例方法总是拥有优先级
  • 实例方法可以到达实例的封装边界,但是扩展函数只能从定义它们的包内方法可见对象过的一部分

扩展函数也可以是一个运算符。在1.2.1中的扩展函数我们可以用in运算符进行扩展

operator fun Circle.contains(point: Point) =
    (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) <
            radius * radius

调用的时候就可以通过in运算符了:

val circle = Circle(10, 10, 25)
val point = Point(5, 10)
println(point in circle)

快速绕开扩展函数,我们也可以添加扩展属性。但是它们不是类内部的一部分,所以扩展属性不能使用幕后字段。下面给Circle添加一个area的属性。

data class Circle(val cx: Int, val cy: Int, val radius: Int)

operator fun Circle.contains(point: Point) =
    (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) <
            radius * radius

val Circle.area: Double
    get() = kotlin.math.PI * radius * radius

fun main() {
    val circle = Circle(10, 10, 25)
    println("Area is ${circle.area}")
}

这里的例子我们定义的是个val的属性,我们也可以定义var的属性,并扩展setter方法;setter方法必须依赖类的其他方法来实现其目标,就像扩展属性的getter一样,扩展属性的setter不能使用幕后字段。

kotlin还可以将扩展函数添加到第三方类中,也可以扩展函数路由到现有的方法。下面一个例子:给String类扩展函数:

fun String.isPalindrome() = reversed() == this
fun String.shut() = uppercase(Locale.getDefault())

isPalindrome()方法使用kotlin的扩展函数reversed()来确定给定的字符串是否是回文。

fun main() {
    val str = "dad"
    println(str.isPalindrome())
    println(str.shut())
}

kotlin还可以通过扩展类的伴生对象将静态方法注入类中,也就是说,将方法注入伴生类对象,而不是类中。

注意:只有类拥有伴生对象时,才可以注入静态方法

fun String.Companion.toURL(link: String) = java.net.URL(link)

fun main() {

    val url: java.net.URL = String.toURL("https://www.baidu.com")
}

向String添加静态或类级方法很简单,但是比不是所有第三方类都可以添加静态方法。例如:我们并不能向JDK的java.net.URL类添加类级方法,因为kotlin并没有向这个类添加伴生类。

上面的例子都是在类外部进行扩展的,kotlin还支持从类内部注入。例子:

class Point(x: Int, y: Int) {
    private val pair = Pair(x, y)
    private val firstSign = if (pair.first < 0) "" else "+"
    private val secondSign = if (pair.second < 0) "" else "+"
    override fun toString() = pair.pointString()

    private fun Pair<Int, Int>.pointString() = "(${firstSign}${first}, ${this@Point.secondSign}${this.second})"
}

在类中定义的扩展函数,甚至类的一个方法,对于缩小在特定或特定方法中使用扩展函数的作用域非常有用。

函数时lambda中对象,你可以将方法注入函数中,就像你可以将方法注入类中一样。在Java函数接口中有一个andThen方法可以组合两个函数,但是Kotlin中不存在,但是我们可以自己定义一个:


fun <T, R, U> ((T) -> R).andThen(next: (R) -> U): (T) -> U = { input: T -> next(this(input)) }

从定义的函数签名可以知道, andThen()添加到一个函数( ((T) -> R))中,该函数接受一个参数化类型 T,并返回类型为 R的结果。传递给 andThen()的参数必须是一个函数,该函数接受一个类型为 R的变量为参数,返回一个参数化类型为 U的结果。

andThen()的主体中,返回了一个 lambda表达式。这个 lambda将其参数传递给调用 andThen()的函数,并将结果传递给 next函数。接着我们添加两个函数使用 andThen函数

fun increment(number: Int): Double = number + 1.toDouble()
fun double(number: Double) = number * 2

fun main() {

    val function = ::increment.andThen(::double)
    println(function(5))
}

在编写的代码中,点和圆括号是很常见的。但是在一些情况省略它们可以使代码不那么混乱,更容易理解。

1.2.2中的例子,我们可以看到调用方式如下:

println(point in circle)
println(circle.contains(point))

这里的@1是因为我们使用了运算符重载,操作符在kotlin中总是自动使用中缀表达法,如果我们也想让@2中也使用中缀表达,此时就需要改动下contains函数,添加一个关键字: infix,如下

operator infix fun Circle.contains(point: Point) =
    (point.x - cx) * (point.x - cx) + (point.y - cy) * (point.y - cy) <
            radius * radius

此时我们就可以使用中缀表达法:

println(circle.contains(point))
println(circle contains point)

infix减少了一些混乱,但是当处理Any对象时, Kotlin更加简洁,更具表现力。

Kotlin有四个重要的方法可以使代码更加流畅: also()apply()let()run(),另外还有一个常用 with,这些看一下这些函数的源码大概就知道用了,下面表格:

函数对象引用返回值是否扩展函数

表达式结果是

表达式结果是

表达式结果不是,调用无需上下文对象

表达式结果不是,把上下文对象当作参数

上下文对象是

上下文对象是

let函数是参数化类型 T的扩展函数。在 let块内可以通过 it指代该对象。返回值为 let块的最后一行或指定 return表达式。

public inline fun <T, R> T.let(block: (T) -> R): R

let在使用中有如下作用:

  • let块中的最后一条语句如果是非赋值语句,则默认情况下它是返回语句,否则返回Unit类型
class Person {
    var name = "Tom"
    var age = 12
    fun play() = "$name -> $age"
}

fun main() {
    println(Person().let {
        it.name = "hello world"
        it.play()
    })
    println(Person().let {
        it.name = "hello world"
    })
}
  • let可用于安全检查, 设置name为一个可空字符串,利用name?.let来进行空判断,只有当name不为空时,逻辑才能走进let函数块中。当你有大量name的属性需要编写的时候,就能发现let的快速和简洁。
class Person(name: String?, val age: Int) {
    constructor(age: Int) : this(null, age)
    var name: String? = null
        get() = field?.let { it } ?: "hello world"
    override fun toString() = "$name -> $age"
}

fun main() {
    println(Person(10))
}
  • let可对调用链的结果进行操作, 就不需要将结果存储在一个单独的变量中,然后打印它。
listOf(1, 2, 3, 4, 5)
        .filter { it > 3 }
        .map { "index->$it" }
        .let { println(it) }

run函数以 this作为上下文对象,且它的调用方式与 let一致并且和 let作用差不多。 run分为两种:扩展函数的 run和非扩展函数的 run

扩展函数run:

public inline fun <T, R> T.run(block: T.() -> R): R

这时可以直接将上面的例子改成run的:

class Person(name: String?, var age: Int) {
    constructor(age: Int) : this(null, age)
    var name: String? = null
        get() = field?.let { it } ?: "hello world"
    override fun toString() = "$name -> $age"
}

fun main() {
    val person = Person(1).run {
            this.age = 10
            this.name = "Tom"
        }
    println(person)
}

非扩展函数run:此时run是没有输入值的,所以此时的run并不能修改对象等操作,但是可以使你在需要表达式的地方执行一个语句。

public inline fun <R> run(block: () -> R): R

例子:

val person2 = run {
        val person1 = Person(1)
        person1.age = 10
        person1.name = "ZZH"
        person1
    }

apply是 T的扩展函数,它将对象的上下文引用为 this,并且提供空安全检查, apply不接受函数块中的返回值,返回的是自己的T类型对象。apply方法可以形成一个方法的链式调用。

public inline fun <T> T.apply(block: T.() -> Unit): T

例子:

class Person(name: String?, var age: Int) {
    constructor(age: Int) : this(null, age)
    var name: String? = null
        get() = field?.let { it } ?: "hello world"
    override fun toString() = "$name -> $age"
}

fun main() {

    val person = Person(1)
        .apply { this.age = 10 }
        .apply { this.name = "Tom" }
    println(person)

    val person = Person(1)
        .apply {
            this.age = 10
            this.name = "Tom"
        }
    println(person)
}

apply函数主要用于初始化或更改对象,因为它用于在不使用对象的函数的情况下返回自身。类似于构建模式。

alsoT的扩展函数,返回值与 apply一致,直接返回 Talso函数的用法类似于 let函数,将对象的上下文引用为 it而不是 this以及提供空安全检查方面。

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

例子:

class Person(name: String?, var age: Int) {
    constructor(age: Int) : this(null, age)
    var name: String? = null
        get() = field?.let { it } ?: "hello world"
    override fun toString() = "$name -> $age"
}

fun main() {
    val person = Person(1).also {
            it.age = 10
            it.name = "Tom"
        }
    println(person)
}

with属于非扩展函数,直接输入一个对象 receiver,当输入 receiver后,便可以更改 receiver的属性,同时,它也与 run做着同样的事情。

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

先一个例子:

class Person(name: String?, val age: Int) {
    constructor(age: Int) : this(null, age)
    var name: String? = null
        get() = field?.let { it } ?: "hello world"
    override fun toString() = "$name -> $age"
}

fun main() {
    val person = Person(10)
    with(person) {
        person.name = "Tom"
    }
    println(person)
}

with使用的是非 null的对象,当函数块中不需要返回值时,可以使用 with

下面总结根据预期目的选择作用域:

  • 对一个非空对象执行lambda表达式: let
  • 将表达式作为变量引入为局部作用域中: let
  • 对象配置: apply
  • 对象配置并计算结果: run
  • 在需要表达式的地方运行语句:非扩展的 run
  • 附加条件: alse
  • 一个对象的一组函数调用: with

不同函数的使用场景是存在折叠的,你可以根据实际情况使用。

二. 创建内部DSL

DSL:是一种旨在特定领域下的上下文的语言。这里的领域是指某种商业上的(例如银行业、保险业等)上下文,也可以指某种应用程序的(例如 Web 应用、数据库等)上下文。

DSL是语境驱动的,并且非常流畅,kotlin语言的流畅性有利于简洁和富于表达的语法。

静态语言类型通常会对语言作为内部DSL宿主的能力造成很大的限制。但是Kotlin是一种非常合适创建内部DSL的语言。

首先kotlin并不坚持使用分号,这对流畅性有明显的好处。放弃分号是创建DSL的第一步;

其次在本文上面的例子中,我们使用过infix关键字,对于中缀表示法的支持是另一个受欢迎的特性。没有点和园括号,而且没有分号,使代码更便于阅读;

还有kotlin可以使用扩展函数,并且扩展函数支持infix关键字注释;

最后,如果函数最后一个参数的类型是lambda表达式,那么你可以将lambda表达式参数放在园括号外;另外如果函数只接受一个lambda表达式作为参数,那么就不需要在调用中使用圆括号,如果函数和类关联,那么可以使用infix关键字去掉点和圆括号。

下面我们使用Kotlin实现一个HTML的DSL,将第一部分的Any的相关函数也使用到,下面下面给出html:

<table>
    <tr>
        <th>序号th>
        <th>名字th>
        <th>年龄th>
    tr>
    <tr>
        <td>1td>
        <td>Tometd>
        <td>10td>
    tr>
table>

先给出最后实现好的DSL:

fun main() {
    table {
        tr {
            th { "序号" }
            th { "名字" }
            th { "年龄" }
        }
        tr {
            td { "1" }
            td { "Tome" }
            td { "10" }
        }
    }.let { println(it) }
}

这里先用一个接口定义一个抽象方法html(),用来输出html标签内容

interface Element { fun html(): String }

接着定义一个td标签存储单元格内容:


class Td(var content: String, var isHead: Boolean): Element {
    constructor(isHead: Boolean = false): this("", isHead)

    override fun html() = if (isHead) "$content" else "$content"
}

在tr标签中,分别定义th和td函数用来构建单元格

class Tr: Element {

    private val children = ArrayList<Td>()

    fun th(block: Td.() -> String) {

        children.add(Td(true).also { it.content = it.block() })
    }
    fun td(block: Td.() -> String) {
        children.add(Td().also { it.content = it.block() })
    }
    override fun html(): String {
        val children = children.joinToString("\n\t\t") { it.html() }
        return "\n\t\t$children\n\t"
    }
}

在table类中,实现hr函数用来实现table标签的功能。

class Table: Element {
    private val children = ArrayList<Tr>()
    infix fun tr(block: Tr.() -> Unit) {
        children.add(Tr().also { it.block() })
    }
    override fun html(): String {
        val children = children.joinToString("\n\t") { it.html() }
        return "\n\t$children\n"
    }
}

此时调用还需要实例化Table对象之后才可以,为了让DSL更加方便,这里还需要实现table方法:

fun table(block: Table.() -> Unit): String {
    return Table().let {
        it.block()
        it.html()
    }
}

到此一个关于html的table的DSL就实现了。

此处我们还可以加入反射,实现自动从数组obbject到table的转化。

三. 编写递归和记忆

递归在编程中扮演着重要的角色,

下面我们先看一个简单的例子:计算某一个数的阶乘,我们可以给出递归和非递归两种解法

fun factorialRec(n: Int): BigInteger =
    if (n  0) 1.toBigInteger()
    else n.toBigInteger() * factorialRec(n - 1)

fun factorialIterative(n: Int): BigInteger =
    (1..n).fold(BigInteger("1")) {
        product, e -> product * e.toBigInteger()
    }

如果此时计算的数很大,递归的方法会增加堆栈,一旦达到危险的级别,程序可能崩溃。

那么如何优化呢?那就是将递归过程编译成迭代过程,这种方式代码表示递归,但它也可以享受迭代的运行时行为,不会发生堆栈溢出的错误。

tailrec fun factorialRec(n: Int): BigInteger =
    if (n  0) 1.toBigInteger()
    else n.toBigInteger() * factorialRec(n - 1)

这里tailrec是指使编译器将递归编译成迭代,如果只加这个关键字编译器会报警告:A function is marked as tail-recursive but no tail calls are found(一个函数被标记为尾部递归,但没有发现尾部调用)

tailrec fun factorial(n: Int,
                         result: BigInteger = 1.toBigInteger()): BigInteger =
    if (n  0) result
    else factorial(n - 1, result * n.toBigInteger())

tailrec优化只适用于可以表示为尾递归的递归,但是如果递归很复杂,那么这样就不太容易,甚至是不可能。

尾递归用优化通过将递归转换尾迭代来控制堆栈的层数。这对效率有影响,但是我们可以通过返回存储值而不是重复调用函数来加快执行速度。

我们不希望程序重新计算函数,那么如果在代码中实现记忆,对同一输入已经执行过了,不论我们对同一输入调用多少次,输出都是一样的。通过保存值,我们可以避免重复计算并加快执行数度。但是需要注意:记忆只能用于纯函数,即没有副作用的函数。

下面看一个常用的斐波那契的计算:

fun fib(n: Int): Long = when(n) {
    0, 1 -> 1L
    else -> fib(n - 1) + fib(n - 2)
}

如果提供的值小于2,那么fib函数返回1,否则需要两次递归调用计算结果。但是fib(4)的调用将是fib(3)和fib(2);但是当计算fib(3)计算时需要fib(2)和fib(1);我们可以发现此时会多次调用fib(2)。对于n小的时候,代码计算速度还是很快的,但是当n增加,计算时间将呈指数级增加。所以这里我们可以通过返回调用结果之前记住它来显著减少计算时间。

Kotlin不直接支持记忆,但是我们可以使用已经了解过的知识来构建。在kotlin中,我们将两种不同的方式来实现记忆:

在Groovy语言中,记忆是作为库的一部分实现的。可以对任何lambda表达式调用memoize()函数,它将返回一个记忆的lambda。

fun <T, R> ((T) -> R).memoize(): ((T) -> R) {
    val original = this
    val cache = mutableMapOf<T, R>()
    return { n: T -> cache.getOrPut(n) { original(n) } }
}

在@1中我们将memoize()方法注入一个通用lambda表达式,该表达式接受一个参数类型为T的参数,并返回一个参数类型R的结果。memoize()函数返回类型是和memoize()注入的方法相同的类型。在memoize()函数中通过map缓冲计算结果。

完整例子:

lateinit var fib: (Int) -> Long

fun <T, R> ((T) -> R).memoize(): ((T) -> R) {
    val original = this
    val cache = mutableMapOf<T, R>()
    return { n: T -> cache.getOrPut(n) { original(n) } }
}

fun main() {
    fib = {  n: Int ->
        when (n) {
            0, 1 -> 1L
            else -> fib(n - 1) + fib(n - 2)
        }
    }.memoize()

    println(measureTimeMillis { fib(40) })
    println(measureTimeMillis { fib(45) })
    println(measureTimeMillis { fib(500) })
}

这里lambda表达式的同一个表达式中即调用又申明。这里lateinit,告诉编译器初始化fib变量,如果没有lateinit,编译器会给一个未给变量赋值的错误。

对于上面的代码可以使用委托来重构上面的实现:

import kotlin.reflect.KProperty
import kotlin.system.measureTimeMillis

class Memoize<T, R>(val func: (T) -> R) {
    val cache = mutableMapOf<T, R>()
    operator fun getValue(thisRef: Any?, property: KProperty<*>) = { n: T ->
        cache.getOrPut(n) { func(n) }
    }
}

val fib: (Int) -> Long by Memoize {  n: Int ->
    when (n) {
        0, 1 -> 1L
        else -> fib(n - 1) + fib(n - 2)
    }
}

fun main() {
    println(measureTimeMillis { fib(40) })
    println(measureTimeMillis { fib(45) })
    println(measureTimeMillis { fib(500) })
}

委托更优雅!

Original: https://blog.csdn.net/yhflyl/article/details/127824908
Author: lucky.麒麟
Title: Kotlin读书笔记之优雅且高效的Kotlin

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/660852/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球