Kotlin 复习篇

2,559 阅读6分钟

Kotlin 的文章已经很多了,这边就不做过多介绍了。说说项目中常用的,以及一些个人的理解吧。顺道复习复习

kotlin 基础操作符

基础这一块就不提了,用过的都熟悉。 image.png

当然非空断言公司是禁用的

但是有一个疑问点,既然任何地方都可以用空安全,那么 lateinit 这个关键字又有什么作用?不知道大家有没有想过这个原因?

其实看过转换后的代码就很容易知道了,示例代码(手打的不一定准确)

var user:User? = null

fun test(){
    user?.name = "1"
}

转换后的代码就是这样

User user;

fun test(){
    if(user != null){
        user.name = "1"
    }
}

就会多出来一个空判断,而 lateinit 就没有这个空判断(代码就不写了)。相比来说 lateinit 性能上会好一点。这也是 lateinit 存在的原因。在你确保对象不会为空的情况下可以使用 lateinit 优化性能。

Kotlin 常用操作符 let、run、apply、also、with

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

@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}


@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

源码分析

let: 是把自己以参数的形式回调出去给使用,并且返回 block (及最后一行),本人一般用于需要传递该对象的时候

run: 是以调用者本身的一个扩展函数的形式执行,并且返回 block (及最后一行),本人一般用于对象执行操作

apply: 是以调用者本身的一个扩展函数的形式执行,并且返回调用者本身,本人一般用于对象创建时赋值

also: 是把自己以参数的形式回调出去给 block 使用,并且返回调用者本身,本人一般用于附加操作

with: 是以第一个参数的一个扩展函数的形式执行,并且返回第一个参数 block (及最后一行),本人一般用于设置时间监听时使用

Kotlin 关键字

inline 关键字、noinline 关键字、crossinline 关键字

inline:用于修饰函数,将函数的实现内联到调用处,可以减少函数调用的开销。

noinline:用于修饰函数参数,表示该参数不会被内联,默认情况下,Kotlin 编译器会将具有 lambda 表达式作为参数的函数进行内联优化,但使用 noinline 关键字可以禁止内联。

crossinline:用于修饰函数参数,表示该参数必须被内联,但是不允许使用 return 语句从函数中返回。(面试时候这个玩意儿问题比较多)

详细的可以看这边文章讲的挺好的 Kotlin inline noinline crossinline 解答

reified

reified:用于修饰泛型类型参数,在运行时获取类型信息。

这个关键字也是面试中常问到的一个并且会提起 java 中类型擦除概念,那么 reifiedjava 类型擦除 有啥关联?

java 类型擦除:在 Java 中,类型擦除(Type Erasure)是指在编译时期,泛型类型信息会被擦除或转换为原始类型。这是由于 Java 泛型的实现方式——类型擦除机制。

Java 的泛型是在 JDK 5 中引入的,它允许我们在编写代码时使用参数化类型,以提高代码的类型安全性和重用性。然而,在编译时,Java 编译器会将泛型类型擦除为其原始类型。

类型擦除的主要目的是为了兼容 Java 泛型的向后兼容性。由于 Java 泛型是在 JDK 5 之后引入的,为了保持与旧版本的 Java 代码相互操作的能力,编译器会将泛型类型擦除为非泛型的形式。

例如,对于一个泛型类 List<T>,编译器会将其中的泛型类型 T 擦除为其上界或者 Object 类型。也就是说,对于编译器来说,List<String>List<Integer> 都会被擦除成 List<Object>

reified 关键字:在 Kotlin 中,使用 reified 关键字可以获取泛型的具体类型信息。

它主要用于内联函数和泛型函数中。通过使用 reified 关键字,我们可以在函数体内部获取泛型的实际类型,并对其进行操作,而不会受到类型擦除的限制。这使得在运行时可以操作泛型的具体类型。

by 关键字

Kotlin 中,什么是委托模式(Delegation Pattern)?如何使用 by 关键字实现属性委托?

委托模式是一种设计模式,其中一个对象(被委托对象)将其某些职责委托给另一个对象(委托对象)。在 Kotlin 中,by 关键字用于实现属性委托。通过使用 by 关键字,我们可以将属性的访问和修改操作委托给另一个对象。

// 假设我们有一个 `User` 类,它有一个属性 `name`,我们希望在每次设置 `name` 属性时打印日志。
// 我们可以使用 `by` 关键字来实现属性委托,将属性的访问和修改操作委托给另一个对象。

class Logger {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Getting ${property.name}")
        return "John Doe"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("Setting ${property.name} to $value")
    }
}

class User {
    var name: String by Logger()
}

// 在上述示例中,我们定义了一个 `Logger` 类,它包含两个方法 `getValue()` 和 `setValue()`,分别用于获取属性值和设置属性值。
// 当我们通过 `User` 对象访问或修改 `name` 属性时,实际上是通过 `Logger` 对象来完成的。

// 例如:
fun main() {
    val user = User()
    println(user.name)     // 输出:Getting name  John Doe
    user.name = "Alice"    // 输出:Setting name to Alice
    println(user.name)     // 输出:Getting name  John Doe
}
// 通过使用 `by` 关键字和属性委托,我们可以在每次访问或修改属性时执行额外的逻辑,例如打印日志。
// 这种方式使得代码更加模块化和可重用,同时保持了类的简洁性和可读性。

对了差点忘了 by 还有个好用的地方

class DotMapHelper @JvmOverloads constructor(hashMap: HashMap<String, Any?> = hashMapOf()) :
    BaseDotMapHelper<DotMapHelper>(hashMap) {
    var moduleId: String? by hashMap.withDefault { null }
    var modulePosition: Int? by hashMap.withDefault { null }
    var position: Int? by hashMap.withDefault { null }
    var type: Int? by hashMap.withDefault { null }
    var elementId: Long? by hashMap.withDefault { null }
    var direction: Int? by hashMap.withDefault { null }
    var url: String? by hashMap.withDefault { null }
}

个人感觉在传参上贼好用,例如埋点的情况。

out 关键字

Kotlin 中,outin 是用来修饰类型参数的关键字,用于定义泛型类和泛型函数的类型约束。

out:用于协变(covariant)类型参数。它允许我们将子类型作为类型参数传递给泛型类或泛型函数。在使用 out 修饰类型参数时,只能将该类型参数作为输出(返回值)类型,不能用于输入(方法参数)类型。换句话说,我们可以从泛型对象中获取数据,但不能将数据存储到泛型对象中。

源码中 out 关键字:

  • List<out T>List 是一个只读接口,使用 out 关键字使其具有协变性。这意味着如果 BA 的子类型,那么 List<B>List<A> 的子类型。这使得我们可以安全地将子类型的列表分配给父类型的列表。

  • Array<out T>Array 是一个固定长度的数组类,使用 out 关键字使其具有协变性。这意味着如果 BA 的子类型,那么 Array<B>Array<A> 的子类型。这使得我们可以安全地将子类型的数组分配给父类型的数组。

  • Iterable<out T>Iterable 是一个只读接口,使用 out 关键字使其具有协变性。这意味着如果 BA 的子类型,那么 Iterable<B>Iterable<A> 的子类型。这使得我们可以安全地将子类型的可迭代对象分配给父类型的可迭代对象。

in 关键字

in:用于逆变(contravariant)类型参数。它允许我们将超类型作为类型参数传递给泛型类或泛型函数。在使用 in 修饰类型参数时,只能将该类型参数作为输入(方法参数)类型,不能用于输出(返回值)类型。换句话说,我们可以将数据存储到泛型对象中,但不能从泛型对象中获取数据。

源码中 in 关键字

  • Consumer<in T>Consumer 是一个接口,使用 in 关键字使其具有逆变性。这意味着如果 BA 的超类型,那么 Consumer<A>Consumer<B> 的超类型。这使得我们可以安全地将超类型的消费者分配给子类型的消费者。

  • Comparable<in T>Comparable 是一个接口,使用 in 关键字使其具有逆变性。这意味着如果 BA 的超类型,那么 Comparable<A>Comparable<B> 的超类型。这使得我们可以安全地将超类型的可比较对象传递给子类型的可比较对象。

关于 out 关键字 和 in 关键字是怎么来的这个可以看这篇文章讲的挺好的 # 一文读懂 kotlin 的协变与逆变 -- 从 Java 说起

这边摘抄部分方便后期

Java 和 C# 早期都是没有泛型特性的。

但是为了支持程序的多态性,于是将数组设计成了协变的。因为数组的很多方法应该可以适用于所有类型元素的数组。

比如下面两个方法:

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

第一个是比较数组是否相等;第二个是打乱数组顺序。

语言的设计者们希望这些方法对于任何类型元素的数组都可以调用,比如我可以调用 shuffleArray(String[] s) 来把字符串数组的顺序打乱。

出于这样的考虑,在 Java 和 C# 中,数组设计成了协变的。

然而,对于泛型来说,却有以下问题:

// Illegal code - because otherwise life would be Bad
List\<Dog\> dogs = new List\<Dog\>();
List\<Animal\> animals = dogs; // Awooga awooga
animals.add(new Cat());// (1)
Dog dog = dogs.get(0); //(2) This should be safe, right?

如果上述代码可以通过编译,即 List<Dog> 可以赋值给 List<Animal>,List 是协变的。接下来往 List<Dog> 中 add 一个 Cat(),如代码 (1) 处。这样就有可能造成代码 (2) 处的接收者 Dog dogdogs.get(0) 的类型不匹配的问题。会引发运行时的异常。所以 Java 在编译期就要阻止这种行为,把泛型设计为默认不型变的。

Kotlin 泛型其实默认也是不型变的,只不过使用 out 和 in 关键字在类声明处型变,可以达到在使用处看起来像直接型变的效果。但是这样会限制类在声明时只能要么作为生产者,要么作为消费者。

作者:牛蛙点点申请出战
链接:juejin.cn/post/688236…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

operator 关键字

以下是一些常见的运算符,以及对应的 operator 表示法:

  • 算术运算符:

    • +operator fun plus(other: T): T
    • -operator fun minus(other: T): T
    • *operator fun times(other: T): T
    • /operator fun div(other: T): T
    • %operator fun rem(other: T): T
  • 比较运算符:

    • ==operator fun equals(other: Any?): Boolean
    • !=operator fun notEquals(other: Any?): Boolean
    • >operator fun compareTo(other: T): Int
  • 逻辑运算符:

    • &&operator fun and(other: Boolean): Boolean
    • ||operator fun or(other: Boolean): Boolean
    • !operator fun not(): Boolean
  • 索引访问运算符:

    • get(index: Int): Toperator fun get(index: Int): T
    • set(index: Int, value: T)operator fun set(index: Int, value: T)
  • 函数调用运算符:

    • invoke(parameters: ...) -> resultoperator fun invoke(parameters: ...): result

infix 关键字

中缀函数允许我们以更具可读性和简洁性的方式调用函数,并且可以在函数名和参数之间使用中缀符号。

中缀函数的语法示例:

infix fun Int.addPlus(num: Int): Int {
    return this + num
}

fun main() {
    val result = 5 addPlus 3 // 使用中缀符号调用中缀函数
    println(result) // 输出:8
}

在上述示例中,我们定义了一个名为 addPlus 的中缀函数,它接受一个整数参数 num,并返回两个整数相加的结果。通过在函数名和参数之间使用中缀符号 infix,我们可以以更简洁的方式调用该函数。

main() 函数中,我们使用中缀符号 addPlus 将整数 53 相加,并将结果赋值给变量 result。然后,我们打印出结果 8

使用中缀函数可以使代码更加自然和易读,特别适用于描述某种关系或操作的情况。需要注意的是,中缀函数必须满足以下条件:

  • 它们必须是成员函数或扩展函数。
  • 它们只有一个参数。
  • 参数不能是可变数量参数(vararg)。
  • 参数和函数本身必须标记为 infix

sealed 关键字

用于修饰类。当一个类被声明为 sealed 时,它只能被同一个文件中的其他类继承,不允许在其他文件中创建该类的子类。

一般与 when 表达式一起使用,以确保在处理所有可能的子类时进行完整的覆盖

sealed class Result<out T>
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()

fun handleResult(result: Result<Any>) {
    when (result) {
        is Success -> println("Success: ${result.data}")
        is Error -> println("Error: ${result.message}")
    }
}

fun main() {
    val successResult = Success(42)
    val errorResult = Error("Something went wrong")

    handleResult(successResult) // 输出:Success: 42
    handleResult(errorResult) // 输出:Error: Something went wrong
}

use 关键字

主要用于处理需要关闭或释放资源的情况,如文件、流和数据库连接等。使用 use 关键字可以确保在代码块执行完毕后自动关闭相关资源,无需手动调用关闭方法。

val file = File("example.txt")

file.inputStream().use { inputStream ->
    // 使用 inputStream 读取文件数据
    // 在代码块结束后,inputStream 会被自动关闭
}

val reader = BufferedReader(FileReader("example.txt"))

reader.use { bufferedReader ->
    // 使用 bufferedReader 读取文件数据
    // 在代码块结束后,bufferedReader 会被自动关闭
}

当代码块执行完毕时,不管是否发生异常,use 关键字会自动关闭相关资源(这里是输入流或缓冲读取器)。

注意,为了能够使用 use 关键字,你需要确保相关资源实现了 Closeable 接口,该接口提供了 close() 方法用于关闭资源。大多数的 I/O 类都实现了 Closeable 接口,包括 InputStreamOutputStreamReaderWriter 等。

使用 use 关键字能够简化代码,并确保及时关闭资源,避免资源泄漏和错误。它是处理资源管理的一种推荐方式。

打完收工不写了,下班铁子们。加油!!!

2023.8.9 21:49