扩展函数和运算符重载

不少现代高级编程语言中有扩展函数这个概念,Java 却一直以来都不支持这个非常有用的功能,这多少会让人有些遗憾。但值得高兴的是,Kotlin 对扩展函数进行了很好的支持,因此这个知识点是我们无论如何都不能错过的。

大有用途的扩展函数

首先看一下什么是扩展函数。扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。

为了帮助你更好地理解,我们先来思考一个功能。一段字符串中可能包含字母、数字和特殊符号等字符,现在我们希望统计字符串中字母的数量,你要怎么实现这个功能呢?如果按照一般的编程思维,可能大多数人会很自然地写出如下函数:

object StringUtil {

    fun letterCount(str: String) : Int {
        var count = 0
        for (char in str) {
            if (char.isLetter()) {
                    count ++
            }
        }

        return count
    }
    
}

这里先定义了一个 StringUtil 单例类,然后在这个单例类中定义了一个 lettersCount() 函数,该函数接收一个字符串参数。在 lettersCount() 方法中,我们使用 for-in 循环去遍历字符串中的每一个字符。如果该字符是一个字母的话,那么就将计数器加 1,最终返回计数器的值。

现在,当我们需要统计某个字符串中的字母数量时,只需要编写如下代码即可:

val str = "ABC123xyz!@#"
val count = StringUtil.lettersCount(str)

这种写法绝对可以正常工作,并且这也是 Java 编程中最标准的实现思维。但是有了扩展函数之后就不一样了,我们可以使用一种更加面向对象的思维来实现这个功能,比如说将 lettersCount() 函数添加到 String 类当中。

下面我们先来学习一下定义扩展函数的语法结构,其实非常简单,如下所示:

fun ClassName.methodName(param1: Int, param2: Int): Int {
    return 0
}

相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个 ClassName. 的语法结构,就表示将该函数添加到指定类当中了。

了解了定义扩展函数的语法结构,接下来我们就尝试使用扩展函数的方式来优化刚才的统计功能。

由于我们希望向 String 类中添加一个扩展函数,因此需要先创建一个 String.kt 文件。文件名虽然并没有固定的要求,但是我建议向哪个类中添加扩展函数,就定义一个同名的 Kotlin 文件,这样便于你以后查找。当然,扩展函数也是可以定义在任何一个现有类当中的,并不一定非要创建新文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

现在在 String.k t文件中编写如下代码:

fun String.lettersCount(): Int {

    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++
        }
    }
    return count
    
}

注意这里的代码变化,现在我们将 lettersCount() 方法定义成了 String 类的扩展函数,那么函数中就自动拥有了 String 实例的上下文。因此 lettersCount() 函数就不再需要接收一个字符串参数了,而是直接遍历 this 即可,因为现在 this 就代表着字符串本身。

定义好了扩展函数之后,统计某个字符串中的字母数量只需要这样写即可:

val count = "ABC123xyz!@#".lettersCount()

是不是很神奇?看上去就好像是 String 类中自带了 lettersCount() 方法一样。

扩展函数在很多情况下可以让 API 变得更加简洁、丰富,更加面向对象。我们再次以 String 类为例,这是一个 final 类,任何一个类都不可以继承它,也就是说它的 API 只有固定的那些而已,至少在 Java 中就是如此。然而到了 Kotlin 中就不一样了,我们可以向 String 类中扩展任何函数,使它的 API 变得更加丰富。比如,你会发现 Kotlin 中的 String 甚至还有 reverse() 函数用于反转字符串,capitalize() 函数用于对首字母进行大写,等等,这都是 Kotlin 语言自带的一些扩展函数。这个特性使我们的编程工作可以变得更加简便。

另外,不要被本节的示例内容所局限,除了 String 类之外,你还可以向任何类中添加扩展函数, Kotlin 对此基本没有限制。如果你能利用好扩展函数这个功能,将会大幅度地提升你的代码质量和开发效率。

有趣的运算符重载

运算符重载是 Kotlin 提供的一个比较有趣的语法糖。我们知道,Java 中有许多语言内置的运算符关键字,如 + - * / % ++ --。而 Kotlin 允许我们将所有的运算符甚至其他的关键字进行重载,从而拓展这些运算符和关键字的用法。

本小节的内容相比于之前所学的 Kotlin 知识会相对复杂一些,但是我向你保证,这是一节非常有趣的内容,掌握之后你一定会受益良多。

我们先来回顾一下运算符的基本用法。相信每个人都使用过加减乘除这种四则运算符。在编程语言里面,两个数字相加表示求这两个数字之和,两个字符串相加表示对这两个字符串进行拼接,这种基本用法相信接触过编程的人都明白。但是 Kotlin 的运算符重载却允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。

当然,虽然 Kotlin 赋予了我们这种能力,在实际编程的时候也要考虑逻辑的合理性。比如说,让两个 Student 对象相加好像并没有什么意义,但是让两个 Money 对象相加就变得有意义了,因为钱是可以相加的。

那么接下来,我们首先学习一下运算符重载的基本语法,然后再来实现让两个 Money 对象相加的功能。

运算符重载使用的是 operator 关键字,只要在指定函数的前面加上 operator 关键字,就可以实现运算符重载的功能了。但问题在于这个指定函数是什么?这是运算符重载里面比较复杂的一个问题,因为不同的运算符对应的重载函数也是不同的。比如说加号运算符对应的是 plus() 函数,减号运算符对应的是 minus() 函数。

我们这里还是以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如下:

class obj {
    operator fun plus(obj: Obj):Obj {
        // 处理相加的逻辑
    }
}

在上述语法结构中,关键字 operator 和函数名 plus 都是固定不变的,而接收的参数和函数返回值可以根据你的逻辑自行设定。那么上述代码就表示一个 Obj 对象可以与另一个 Obj 对象相加,最终返回一个新的 Obj 对象。对应的调用方式如下:

val obj1 = Obj()
val obj2 = Obj()
val obj3 = obj1 + obj2

这种 obj1 + obj2 的语法看上去好像很神奇,但其实这就是 Kotlin 给我们提供的一种语法糖,它会在编译的时候被转换成 obj1.plus(obj2) 的调用方式。

了解了运算符重载的基本语法之后,下面我们开始实现一个更加有意义功能:让两个 Money 对象相加。

首先定义 Money 类的结构,这里我准备让 Money 的主构造函数接收一个 value 参数,用于表示钱的金额。创建 Money.kt 文件,代码如下所示:

class Money(val value: Int)

定义好了 Money 类的结构,接下来我们就使用运算符重载来实现让两个 Money 对象相加的功能:

class Money(val value: Int) {
    operator fun plus(money: Money): Money {
        val sum = value + money.value
        return Money(sum)
    }
}

可以看到,这里使用了 operator 关键字来修饰 plus() 函数,这是必不可少的。在 plus() 函数中,我们将当前 Money 对象的 value 和参数传入的 Money 对象的 value 相加,然后将得到的和传给一个新的 Money 对象并将该对象返回。这样两个 Money 对象就可以相加了,就是这么简单。

现在我们可以使用如下代码来对刚刚编写的功能进行测试:

val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
println(money3.value)

但是,Money 对象只允许和另一个 Money 对象相加,有没有觉得这样不够方便呢?或许你会觉得,如果 Money 对象能够直接和数字相加的话,就更好了。这个功能当然也是可以实现的,因为 Kotlin 允许我们对同一个运算符进行多重重载,代码如下所示:

class Money(val value: Int) {
    operator fun plus(money: Money): Money {
        val sum = value + money.value
        return Money(sum)
    }

    operator fun plus(newValue: Int): Money {
        val sum = value + newValue
        return Money(sum)
    }
}

这里我们又重载了一个 plus() 函数,不过这次接收的参数是一个整型数字,其他代码基本是一样的。

那么现在,Money 对象就拥有了和数字相加的能力:

val money1 = Money(5)
val money2 = Money(10)
val money3 = money1 + money2
val money4 = money3 + 20
println(money4.value)

这里让 money3 对象再加上 20 的金额,最终打印的结果就变成了 35。

当然,你还可以对这个例子进一步扩展,比如加上汇率转换的功能。让 1 人民币的 Money 对象和 1 美元的 Money 对象相加,然后根据实时汇率进行转换,从而返回一个新的 Money 对象。这类功能都是非常有趣的,运算符重载如果运用得好的话,可以玩出很多花样。

前面我们花了很长的篇幅介绍加号运算符重载的用法,但实际上 Kotlin 允许我们重载的运算符和关键字多达十几个。显然这里我不可能将每一种重载的用法都逐个进行介绍,因此我在下表中列出了所有常用的可重载运算符和关键字对应的语法糖表达式,以及它们会被转换成的实际调用函数。如果你想重载其中某一种运算符或关键字,只要参考刚才加号运算符重载的写法去实现就可以了。

语法糖表达式实际调用函数
a+ba.plus(b)
a-ba.plus(b)
a * ba.times(b)
a / ba.div(b)
a % ba.rem(b)
a++a.inc()
a--a.dec()
+aa.unaryPlus()
-aa.unaryMinus()
!aa.not()
a == ba.equals(b)
a > ba.equals(b)
a < ba.equals(b)
a >= ba.equals(b)
a <= ba.compareTo(b)
a..ba.rangeTo(b)
a[b]a.get(b)
a[b] = ca.set(b, c)
a in bb.contains(a)

那么关于运算符重载的内容就学到这里。接下来,我们结合刚刚学习的扩展函数以及运算符重载的知识,对之前编写的一个小功能进行优化。

回想一下,在第 4 章和本章中,我们都使用了一个随机生成字符串长度的函数,代码如下所示:

fun getRandomLengthString(str: String): String {
    val n = (1..20).random()
    val builder = StringBuilder()
    repeat(n) {
        builder.append(str)
    }
    return builder.toString()
}

其实,这个函数的核心思想就是将传入的字符串重复 n 次,如果我们能够使用 str * n 这种写法来表示让 str 字符串重复 n 次,这种语法体验是不是非常棒呢?而在 Kotlin 中这是可以实现的。

先来讲一下思路吧。要让一个字符串可以乘以一个数字,那么肯定要在 String 类中重载乘号运算符才行,但是 String 类是系统提供的类,我们无法修改这个类的代码。这个时候就可以借助扩展函数功能向 String 类中添加新函数了。

既然是向 String 类中添加扩展函数,那么我们还是打开刚才创建的 String.kt 文件,然后加入如下代码:

operator fun String.times(n: Int): String {
    val builder = StringBuilder()
    repeat(n) {
        builder.append(this)
    }
    return builder.toString()
}

这段代码应该不难理解,这里只讲几个关键的点。首先,operator 关键字肯定是必不可少的;然后既然是要重载乘号运算符,参考上表可知,函数名必须是 times;最后,由于是定义扩展函数,因此还要在方向名前面加上 String. 的语法结构。其他就没什么需要解释的了。在 times() 函数中,我们借助 StringBuilder 和 repeat 函数将字符串重复 n 次,最终将结果返回。

现在,字符串就拥有了和一个数字相乘的能力,比如执行如下代码:

val str = "abc" * 3
println(str)

最终的打印结果是:abcabcabc。

另外,必须说明的是,其实 Kotlin 的 String 类中已经提供了一个用于将字符串重复 n 遍的 repeat() 函数,因此 times() 函数还可以进一步精简成如下形式:

operator fun String.times(n: Int) = repeat(n)

掌握了上述功能之后,现在我们就可以在 getRandomLengthString() 函数中使用这种魔术一般的写法了,代码如下所示:

fun getRandomLengthString(str: String) = str * (1..20).random()

怎么样,有没有觉得这种语法用起来特别舒服呢?只要你能灵活使用本节学习的扩展函数和运算符重载,就可以定义出更多有趣且高效的语法结构来,本书在后续章节中也会对这部分功能进行更多的拓展。


Maenj_Ba_lah
28 声望5 粉丝

真正的大师,永远怀着一颗学徒的心。