一文了解kotlin中的关键字

912 阅读7分钟

屏幕截图 2024-06-20 233155.png

在 Kotlin 开发中,我们会使用或者见到各种各样的关键字。一些比较常见,像 opencompanioninner等;一些就比较冷门,像operatorinfixnoinline等等。这篇文章就介绍这些用得比较少得关键字。

operator

operator 关键字作用是将一个函数标记为重载一个操作符或者实现一个约定。虽然在开发过程中用到的比较少,但是实际上在 kotlin 库中,它的作用非常大。

重载操作符

在 Kotlin 协程中,CoroutineContext 就用到 operator 关键字。源码如下所示,了解即可不用关心具体的实现逻辑。

public interface CoroutineContext {
    ...
    
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    // make sure interceptor is always last in the context (and thus is fast to get when present)
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }

}

可以看到,CoroutineContext 重载了 + 操作符,这样我们就可以在代码中使用 +,来连接两个 CoroutineContext了。

除了 + 操作符之外,还有很多操作符可以重载。这里分成两类,一类是数字相关的操作符,一类是集合相关的操作符。可以重载的数字操作符有:

表达式对应方法
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a++a.inc()
a--a.dec()
a + ba.plus(b)
a - ba.minus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a += ba.plusAssign(b)
a -= ba.minusAssign(b)
a *= ba.timesAssign(b)
a /= ba.divAssign(b)
a %= ba.remAssign(b)
a > ba.compareTo(b) > 0
a < ba.compareTo(b) < 0
a >= ba.compareTo(b) >= 0
a <= ba.compareTo(b) <= 0
a == ba?.equals(b) ?: (b === null)
a != b!(a?.equals(b) ?: (b === null))

可以重载的集合操作符有:

表达式对应方法
a..ba.rangeTo(b)
a..<ba.rangeUntil(b)
a in bb.contains(a)
a !in b!b.contains(a)
a[i]a.get(i)
a[i, j]a.get(i, j)
a[i_1, ……, i_n]a.get(i_1, ……, i_n)
a[i] = ba.set(i, b)
a[i, j] = ba.set(i, j, b)
a[i_1, ……, i_n] = ba.set(i_1, ……, i_n, b)

自定义委托的声明

在开发过程中,我们使用 by 关键字就可以简单地实现委托功能,比如by viewModels。但是这些常用的委托的效果,都是库中实现的。如果我们想要自定义委托的效果,就需要用到 operator。如果你之前不了解 kotlin 的委托,可以看一文理解 Kotlin 的委托

代码示例如下:

class Example {
    var p: String by Delegate()
}


class Delegate {
    // operator 表明 getValue 委托对应属性的 get 方法
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return ""
    }
    // operator 表明 setValue 委托对应属性的 set 方法
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
           
    }
}

对象解构

在 kotlin 中,支持对象的解构。解构是指把一个对象解构成多个变量。代码示例如下:

val (name, age) = person

如果你想使自己的类支持解构,就需要创建 componentN 方法,代码示例如下:

val name = person.component1()
val age = person.component2()

// 内部定义,需要带上 operator
operator fun component1(): String {
    return name
}

operator fun component2(): Int {
    return age
}

infix

带有 infix 关键字的方法,可以使用中缀表示法调用。中缀表示法是指忽略该调用的点与圆括号。代码示例如下:

class MyStringCollection {
    infix fun add(s: String) { /*……*/ }

    fun build() {
        this add "abc"   // 正确
        add("abc")       // 正确
        //add "abc"        // 错误:必须指定接收者
    }
}

需要注意的是,中缀方法必须满足以下要求:

  1. 必须是成员函数或扩展函数。
  2. 必须只有一个参数。
  3. 其参数不得接受可变数量的参数且不能有默认值。

inline、noinline、crossinline

inline 可以让你用内联,即将方法块的代码直插到调用处,代码示例如下:

fun method(action: () -> Unit)

fun main() {
    repeat(9) {
        method {
            doSomething()
        }
    }
}

// method 加了 inline后,会直接将 doSomething() 放到调用的地方
fun main() {
   repeat(9) {
      doSomething() 
   } 
}

之所以需要 inline 这个关键字,是因为高阶函数实际上是通过创建对象来实现的。如果在循环中使用,会创建大量的对象,加上了inline 这个关键字就可以减少函数类型的对象的创建。

noinline 关键字可以局部关掉这个优化,代码示例如下:

inline fun method(action: () -> Unit, noinline result: () -> Int): () -> Int {
    action()
    return result
}

我们之前能够返回函数,这是因为高阶函数在 kotlin 中,是用对象实现的。如果使用了 inline 这个关键字,就不可以返回了。如果你需要局部返回,就可以使用 noinline 这个关键字来局部关闭这个优化。

最后 crossinline 关键字是局部加强这个优化,让内联函数里的函数类型的参数可以被当做对象使用。代码示例如下:

inline fun method(crossinline action: () -> Unit) {
    method1 {
        // 加上 crossinline 后就可以作为对象使用,但是无法返回
        action()
    }
    // 无法作为返回值
    // return action
}

fun method1(action1: () -> Unit) {  
    action1()  
}

var、val、const、final

在 kotlin 中 var 表示是可变量,而 val 表示是只读变量,const 表示的是编译时常量,kotlin 中的 final 表示的是禁止子类覆盖。

kotlin中的 val 关键字修饰的变量与 java 中 final 修饰的变量作用是相同的。但是 val 只是表示可读变量,即该变量无法修改。但是这不意味着它不可变,如果我们自定义变量的 get 方法就可以让获取的值不一样。示例如下:

var k = 1  
  
val j: Int  
    get() {  
        return if(k > 0) 1 else 0  
    }

如果要表示编译时常量,即不可变的量,需要通过 const val 来修饰的。const 只能修饰没有自定义 getter 的 val 属性,而且它的值必须在编译时确定,而且 const 只能在单例或者伴生对象中使用。

而 kotlin 中的 final 与变量和常量没有关系,它的作用是禁止子类覆盖,代码示例如下:

open class Rectangle() : Shape() {
    // 禁止覆盖该方法
    final override fun draw() { /*……*/ }
}

in、out、where、reified

inoutwherereified 关键字都用在泛型中。其中:

  • in 表示声明处的逆变,等同于 Java 中的 <? super Object>
  • out 表示声明处的协变,等同于 Java 中的 <? extends T>
  • where 可以来限制泛型,等同于Java中的 T extends Object
  • reified 为内联函数支持具体化的类型参数

其中 in 关键字除了用于泛型之外,还可以在for 循环中迭代的对象;或者作为中缀操作符,判断一个值是否属于一个区间或者一个集合。

reified 只能在被关键字 inline 修饰的函数中使用。代码示例如下:

// 不使用 reified 关键字
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
    var p = parent
    while (p != null && !clazz.isInstance(p)) {
        p = p.parent
    }
    @Suppress("UNCHECKED_CAST")
    return p as T?
}

// 使用 reified 关键字,注意需要加上 inline
inline fun <reified T> TreeNode.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) {
        p = p.parent
    }
    return p as T?
}

inline fun <reified T> membersOf() = T::class.members

可以看到被reified 修饰的泛型 T 可以被当作普通对象来处理。

actual、expect

actualexpect 是用于 kotlin 多平台的关键字。其中actual 表示多平台项目中的一个平台相关实现;而expect 将一个声明标记为平台相关,并期待在平台模块中实现。

vararg 和 *

vararg 表示可变数量的参数,等同于 Java的 method(Obj... objs);而 * 是展开操作符,可以将数组内容展开。代码示例如下:

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts 是一个数组
        result.add(t)
    return result
}


val list = asList(1, 2, 3)
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

tailrec

tailrec 用于尾递归的优化,Kotlin - 尾递归优化 介绍得很清楚了,具体可以看这篇文章。

typealias

typealias 关键字用来声明一个别名。代码示例如下:

// 减少命名的长度
typealias FileTable<K> = MutableMap<K, MutableList<File>>

// 为函数类型命名
typealias MyHandler = (Int, String, Any) -> Unit

参考