扩展函数
什么是扩展函数?
扩展函数是 Kotlin 提供的功能,可以让我们在不修改某个类源码的情况下,向这个类中"添加"新的函数。
例如,当前有一个功能需要统计字符串中字母的数量。一般来说,你可能会这么完成:
kotlin
object StringUtil {
fun lettersCount(str: String): Int {
var count = 0
for (char in str) {
if (char.isLetter()) {
count++
}
}
return count
}
}
我们创建了一个工具类 StringUtil
,在其中定义了一个 lettersCount()
方法,它会接收一个字符串参数,然后返回该字符串中字母的数量。
使用时,只需通过 StringUtil.lettersCount()
这种形式来调用即可,例如:
kotlin
fun main() {
val str = "ABC123xyz!(@)#+-/*."
val count = StringUtil.lettersCount(str)
println("The number of letters in the string is $count")
}
这种方法虽然可行,但却不够面向对象,扩展函数可以让我们的调用方式变得更加自然。
扩展函数的语法
我们先来看看扩展函数的语法格式:
kotlin
fun ClassName.methodName(param1: Any, param2: Any): ReturnType {
// 函数体
}
它只是在普通函数的函数名前面加上了一个 ClassName
类名,其中 ClassName
就是我们要扩展的类,这样该函数就会被"添加"到 ClassName
类中了。
现在,我们使用扩展函数来对之前 lettersCount
的实现进行优化,创建一个 String.kt
文件,代码如下:
文件名虽然没有强制的要求,但最好为扩展类的类名,方便查找。你也可以再加上一个
Extensions
后缀。
kotlin
fun String.lettersCount(): Int {
var count = 0
for (char in this) {
if (char.isLetter()) {
count++
}
}
return count
}
注意:由于当前 lettersCount()
函数是 String 类的扩展函数,函数中自动拥有了 String 实例的上下文,无需通过函数参数来接收字符串实例了。所以我们这里遍历的是 this
,它代表的就是调用该函数的 String 实例。
有了这个扩展函数后,我们只需这样调用:
kotlin
fun main() {
val str = "ABC123xyz!(@)#+-/*."
val count = str.lettersCount()
println("the count of letter in str is $count")
}
就像是 String
类中自带了 lettersCount
方法一样。
扩展函数更加面向对象,可以让 API 变得更加简洁且丰富。
扩展函数的原理
其实从原理上,扩展函数并没有添加到类中,也就是并没有真正修改类的源码。它本质上是一个静态函数,编译器会将类的实例作为扩展函数的第一个参数传递进去。
以上述的 str.lettersCount()
为例,在编译后,会被转换成 StringKt.lettersCount(str)
。
运算符重载
运算符重载是 Kotlin 提供的特性,它允许我们为自定义类型重载运算符,从而以更简洁、更符合直觉的方式进行运算。每个可重载的运算符都有对应的函数名。
运算符重载使用的关键字是 operator
。只要在指定的函数前加上 operator
关键字,即可实现对特定运算符的重载。
我们以加号运算符为例,它对应的函数是 plus()
。我们来为一个类重载加号:
kotlin
class Obj {
operator fun plus(obj: Obj): Obj {
// 处理相加的逻辑
return Obj()
}
}
当我们调用如下代码时:
kotlin
val obj3 = obj1 + obj2
实际上编译器会把它转换为:val obj3 = obj1.plus(obj2)
。
这其实就是 Kotlin 给我们提供的语法糖。
典型例子
了解了运算符的基本语法后,我们再来看一个加号运算符的例子。
首先定义数据类 Point
,其中 Point 表示二维中的点。
kotlin
data class Point(val x: Int, val y: Int) {
}
然后在其中重载加号运算符,并实现两个点的向量相加。
kotlin
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
val x = this.x + other.x
val y = this.y + other.y
// 创建并返回一个新的 Point 实例
return Point(x, y)
}
}
然后测试一下:
kotlin
fun main() {
val p1 = Point(2, 3)
val p2 = Point(4, 1)
val p3 = p1 + p2
println("p1 + p2 = p3: $p3") // 输出结果: p1 + p2 = p3: Point(x=6, y=4)
}
并且我们还可以对加号运算符进行多重重载,比如让 Point 对象加上一个整数,如下所示:
kotlin
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
val x = this.x + other.x
val y = this.y + other.y
// 创建并返回一个新的 Point 实例
return Point(x, y)
}
operator fun plus(value: Int): Point {
val x = this.x + value
val y = this.y + value
// 创建并返回一个新的 Point 实例
return Point(x, y)
}
}
测试一下:
kotlin
fun main() {
val p1 = Point(2, 3)
val p3 = p1 + 5
println("p1 + 5 = p3: $p3") // 输出结果: p1 + 5 = p3: Point(x=7, y=8)
}
可重载的运算符
当然可重载的运算符不止这一种,我们来看看语法糖表达式和实际调用函数的对照表:
语法糖表达式 | 实际调用函数 |
---|---|
a + b |
a.plus(b) |
a - b |
a.minus(b) |
a * b |
a.times(b) |
a / b |
a.div(b) |
a % b |
a.rem(b) |
a++ |
a.inc() |
a-- |
a.dec() |
+a |
a.unaryPlus() |
-a |
a.unaryMinus() |
!a |
a.not() |
a == b |
a.equals(b) |
a > b ,a < b ,a >= b ,a <= b |
a.compareTo(b) |
a..b |
a.rangeTo(b) |
a[b] |
a.get(b) |
a[b] = c |
a.set(b, c) |
a in b |
b.contains(a) |
综合实战
了解完扩展函数以及运算符重载后,我们将它们综合在一起,来对之前获取随机重复次数字符串的函数进行优化,之前的代码:
kotlin
fun getRandomLengthString(str: String): String {
val n = (1..20).random()
val builder = StringBuilder().apply {
repeat(n) {
append(str)
}
}
return builder.toString()
}
这个函数主要是将传入的字符串重复 n 次,我们来将重复字符串的操作改造为使用 str * n 这种写法。
要让字符串可以与数字进行相乘,所以需要在 String 类中重载乘号运算符,而 String 是系统库中的类,无法修改其源码。但我们可以借助扩展函数来完成。
来到之前创建的 String.kt
文件中,增加如下代码:
kotlin
operator fun String.times(n: Int): String {
val builder = StringBuilder().apply {
repeat(n) {
append(this@times) // 注意
}
}
return builder.toString()
}
现在字符串就可以和数字相乘了。
注意:因为使用了 apply
函数,所以在其内部的 this 指代的是前面的 StringBuilder 实例,要想获取 String 实例,需要使用 this@Xxx
的语法,来明确指定当前要访问的是哪一个函数的接受者对象。
所以之前的 getRandomLengthString()
函数可以简化为:
kotlin
fun getRandomLengthString(str: String) = str * (1..20).random()
测试一下:
kotlin
fun getRandomLengthString(str: String) = str * (1..20).random()
fun main() {
val str =getRandomLengthString("Whoa! ")
println(str)
}
程序的运行结果可能是:Whoa! Whoa! Whoa!
另外,其实 String 类中已经提供了一个 repeat()
函数,它可以将字符串重复 n 次,所以我们的 times()
函数还可以进行简化:
kotlin
operator fun String.times(n: Int) = repeat(n)