Kotlin 拓展函数

kotlin 的拓展函数,为这个语言带来极大的可玩性。可以极大的减少现有类的大小, 比如 string这个类 在Android 的 java实现中 有三千多行

但是在 Kotlin 版本中的 String类只有50多行,不到一屏。是不是匪夷所思

那,我们同样调用的那些 string 的方法是怎么做到无痛调用的呢? 答案就是- 函数拓展。

kotlin 的 string 的 function 一部分的实现都在kotlin.text.Strings.kt中作为拓展函数来实现,还有一些是在StringsJVM.kt 中实现。

所以我们利用拓展函数也能实现基本的拓展方法,也能为现有的类新增新的方法(本质上是一个顶层方法,类似于静态方法,到处都能引用)比如为字符串增加一个判断是否是邮箱的拓展方法如下。

// 自定义 String 类的扩展函数,用于判断是否是合法的电子邮件格式
fun String.isValidEmail(): Boolean {
    // 简单的正则表达式,用于匹配电子邮件格式
    val emailRegex = "^[A-Za-z](.*)([@])(.+)(\\.)(.+)".toRegex()
    return this.matches(emailRegex)
}

fun main() {
    val email = "example@example.com"
    val invalidEmail = "example.com"

    // 使用自定义的扩展函数来判断字符串是否为合法的电子邮件
    println(email.isValidEmail()) // 输出: true
    println(invalidEmail.isValidEmail()) // 输出: false
}

但是也有局限性,就是扩展函数不能重写父类的成员函数,且它们在继承结构中不会被子类覆盖。

open class Parent
class Child : Parent()

fun Parent.foo() = "Parent"
fun Child.foo() = "Child"

fun main() {
    val p: Parent = Child()
    println(p.foo())  // 输出: "Parent" 而不是 "Child"
}

拓展函数也能拓展属性,但本质上只是一个 get/set 方法,不会真正修改对象的内存结构。

val String.firstChar: Char
    get() = this[0]

fun main() {
    val str = "Hello"
    println(str.firstChar)  // 输出: 'H'
}

这种机制让 Kotlin 既保持了与 Java 的兼容性,同时又提供了极大的灵活性,可以让开发者在不修改现有类的情况下为其添加新功能。

正儿八经的事情说完了,但是今天要介绍的是一些奇技淫巧。下面开始

1.解构(destructure)数据

什么是解构,我们使用一对括号,放入我们的变量,然后直接=一个对象,就是一段解构声明。Kotlin会自动按照类变量声明的顺序,将相关的值取出赋值给=左边的变量。看一下如下例子就懂了

data class User(val id:Int, val name:String)
fun main() {
    val user = User(10086, "hugh")
    val (uid, name) = user
    println(uid)//10086
    println(name)//hugh
    println(user.component1())//10086
    println(user.component2())//hugh

 }

还可以在 map 中使用,可能遍历一个映射(map)最好的方式就是这样:

for ((key, value) in map) {
   // 使用该 key、value 做些事情
}

首先它是怎么实现的呢? 标准库提供了这样的扩展:

operator fun <K, V> Map<K, V>.iterator(): Iterator<Map.Entry<K, V>> = entrySet().iterator()
operator fun <K, V> Map.Entry<K, V>.component1() = getKey()
operator fun <K, V> Map.Entry<K, V>.component2() = getValue()

上面对应的拓展了操作符 for/函数名iterator 和操作符 ()/ 解构声明 component1

所以使用 component 我们可以对任何数据进行解构,比如要想实现如下数据

根据提示,我们可以实现拓展 LocalDate 类的 操作符 component1,component2,component3

operator fun LocalDate.component1()= year
operator fun LocalDate.component2()= monthValue
operator fun LocalDate.component3()= dayOfMonth

以上就能使编译通过,并且正确执行了。我们不一定需要定义成 data class

2. 字符串也变成函数?/invoke

  "hello world"()

我们可以拓展一个对象的invoke方法,来实现对对象进行调用。

operator fun Any?.invoke() {
    println(this)
}

上面例子可以让任意对象变成一个函数调用,都可以在控制台输出

    "hello world"()
    1()
    null()
    true()

于是,便可以实现如下效果

    val exp = 3(2 + 6)
    println(exp)

3. 通过索引来找目标/ get

你应该写过如下的代码

    val list = arrayListOf(1, 2, 3)
    println(list[1])

如果想通过如下代码从一个数字中,找到第几位,比如从 321 中提取出个位,十位,百位如下代码是不是看起来很简洁

    val count = 123
    println(count[3])//0
    println(count[2])//1
    println(count[1])//2

它可以通过如下方式重载操作符,让上面表达式生效

operator fun Int.get(idx: Int): Int = div(10.0.pow(idx.toDouble())).rem(10).toInt()

除了对变量进行拓展 get 方法,也可以对函数表达式进行拓展

 val double: (Int) -> Int = { it * 2 }
 println(double(12))

让这个变量也可以实现奇怪的表达式

    operator fun ((Int) -> Int).get(idx:Int):Int{
        return this(idx)
    }
    println(double[12])//24
    operator fun ((Int) -> Int).invoke(call: () -> Int): Int {
        return this(call())
    }
    print(double { 12 })//24

4. 像自然语言一样写语句 /contains,in

回到之前的例子

    val data = LocalDate.parse("2024-09-17")

如果我们需要判断这个日期在不在哪一年哪一月,可以用如下语句判断

 operator fun Month.contains(date: LocalDate): Boolean {
        return this == date.month
    }

    operator fun Int.contains(date: LocalDate): Boolean {
        return this == date.year
    }

    infix fun Month.of(year: Int): Pair<Month, Int> = this to year //中缀函数

    operator fun Pair<Month, Int>.contains(date: LocalDate): Boolean {
        return this.first == date.month && this.second == date.year
    }
    println(data in Month.OCTOBER)//false
    println(data in 2024)//true
    println(data in Month.SEPTEMBER of 2024)//true
    val p = Month.MAY of 2023

这个例子,拓展了 年月份的判断 contains, 使用中缀函数,增加了 of 来返回一个 pair 再拓展 pair 的contains 方法就可以达到 像英语一样写表达式了。

结语

Kotlin 中提供了一套常见的可重载操作符,以下是一些常用的操作符及其对应的函数:

在 Kotlin 中,operator 关键字用于重载操作符。这意味着你可以自定义类的某些操作符行为,使得你的类可以像基础类型一样使用操作符(例如 +, -, *, [], ++, -- 等)。

Kotlin 中的操作符重载并不是修改现有的操作符,而是通过定义一些特殊的函数(通常带有 operator 关键字),让你能够为自定义的类赋予特定的操作符行为。Kotlin 提供了一套可以重载的操作符列表,每个操作符都有其对应的函数名称。

常见的可重载操作符

Kotlin 中提供了一套常见的可重载操作符,以下是一些常用的操作符及其对应的函数:

操作符对应函数名示例
+plusa + b
-minusa - b
*timesa * b
/diva / b
%remmoda % b
++inca++
--deca--
[]get / seta[i] / a[i] = value
==equalsa == b
>compareToa > b
incontainsa in b

Leave a Reply

Your email address will not be published. Required fields are marked *