「码上开学——hencoder」Kotlin笔记(会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions )

1,270 阅读13分钟

开始

Kotlin有个特别好用的功能叫扩展,你可以给已有的类去额外添加函数和属性,而且既不需要改源码也不需要写子类。另外很多人虽然会用扩展,但只会最基本的使用,比如就组织用来写个叫dp的扩展属性来把dp值转成像素值。

val Float.dp
    get() = TypedValue.applyDimension(
        TypedValue.COMMPLEX_UNIT_DIP,
        this,
        Resources.getSystem().displayMetrics
    )
...
val RADIUS = 200f.dp

稍微高级一点就不太行了,尤其是扩展函数和函数引用混在一起的时候就更是瞬间蒙圈。

Java的Math.pow()

在Java里我们如果想做幂运算——也就是几的几次方——要用静态方法pow(a, n)

Math.pow(2, 10);// 2的10次方

pow这个词你可能不认识,其实她不是个完美的词,而是power的缩写,power就是乘方的意思,哎中国人学程序经常还需要学英文好烦。这个pow(a, n)方法是Math类的一个静态方法,这类方法我们用得比较多的是max()min()

Math.max(1, 2);// 2
Math.min(1, 2);// 1

比较两个数的大小,用静态方法很符合直觉;但是幂运算的话,静态方法就不如成员方法来的更直观了:

2.pow(10);//要是Java里能这样写就好了

但我们只能选择静态方法。为什么?很简单,因为Integer、Float、Double这几个类没提供这个方法,所以我们只能用Math类的静态方法。

Kotlin的扩展函数Float.pow()

在Kotlin里,我们用的不是Java的Integer、Flout、Double,而是另外一个名字相同或相像的Kotlin自己新创造的类。这几个类同样没有提供pow这个函数,但好的是,我们依然可以用看起来像是成员函数的方式来做幂运算。

2f.pow(10)// Kotlin可以这么写

为什么?因为Float.pow(n: Int)是Kotlin给Float这个类增加的一个扩展函数:

// kotlin.util.MathJVM.kt
 public actual inline fun Float.pow(n: Int): Float = nativeMath.pow(this.toDouble(), n.toDouble()).toFloat()

在声明一个函数的时候在函数名的左边写一个类名再加个点,你就能对这个类的对象调用这个函数了。这种函数就叫扩展函数,Extension Functions。就好像你钻到这个类的源码里,改了它的代码,给它增加了一个新的函数一样。虽然事实上不是,但用起来基本一样。具体区别我等会儿说。

这种用法给我们的开发带来了极大的便利,我们可以用它来做很多事。

举个例子?

  • 比如pow()吧?
  • 再比如,AndroidX里有个东西叫ViewModel对吧?——这个以后有空的话也讲一下,很多人对ViewModel有很大误解,竟然以为这是用来写MVVM架构——AndroidX的KTX库里有一个对于ComponentActivity类的扩展函数叫viewModels(): image.png 只要引用了对应的KTX库,在Activity里你可以直接就调用这个函数来很方便地初始化ViewModel:
class MainActivity: AppCompatActivity() {
    val model: MyViewModel by viewModels()
    ...
}

而不需要重写Activity类。 类似的用法可以有很多很多,限制你的是你的想象力。所以其实对于扩展函数,你更需要注意的是谨慎和克制:需要用了再用,而不要因为它很酷很方便能用则用。因为这些方便的东西如果太多,就会变成对你和同事的打扰。

扩展函数的写法

扩展函数卸载哪都可以,但写的位置不同,作用域就也不同。所谓制作用于就是说你能在哪些地方调用它。

最简单的写法就是把它写成Top Level也就是顶层的,让它不属于任何类,这样你就能在任何类里使用它 。这也和成员函数的作用域很像-哪里能用到这个类,哪里就能用到类里的这个函数:

package com.rengwuxian

fun String.method1(i: Int) {
    ...
}
...
"rengwuxian".mmethod1(1)

有一点要注意了:这个函数属于谁?属于函数左边的类吗?并不是,它是个Top-level Function,它谁也不属于,或者说它只属于它所在的package。那它为什么可以被这个类的对象调用呢?——因为它在函数名的左边呀!在Kotlin,当你给声明的函数命名左边加上一个类名的时候,表示你要给这个函数限定一个Receiver——直译的话叫接收者,其实也就是哪个类的对象可以调用这个函数。虽然说你是个Top-level Function,不属于任何类——确切地说是,不是任何一个类的成员函数——但我要限制只有通过某个类的对象才能调用你。这就是扩展函数的本质。

拿着……和成员函数有什么区别吗?这种奇怪又绕脑子的知识有什么用吗?

成员扩展函数

除了写成Top Level的,扩展函数也可以写在某个类里:

class Example {
    fun String.method2(i: Int) {
        ...
    }
}

然后你就可以在这个类里调用这个函数,但必须使用那个前缀类的对象来调用它:

class Example {
    fun String.method2(i: Int) {
        ...
    }
    ...
    "rengwuxian".method2(1// 可以调用
}

看起来……有点奇怪了。这个函数这么写,它到底是属于谁的呀?属于外部的类还是左边前缀的类?

属于谁?这个「属于谁」其实有点模糊了,我需要问再明确点:它是谁的成员函数?当然是外部的类的成员函数了,因为它写在它里面嘛,对吧?那函数名左边是什么?刚才我刚说过,它是这个函数Receiver,对吧?也就是谁可以去调用它。

所以它及时外部类的成员函数,又是前缀类的扩展函数。

这种既是外部类的成员函数,又是前缀类的扩展函数,它们的用法跟Top Level的扩展函数一样,只是由于它同时还是成员函数,所以只能在它所属的类里面被调用,到了外面就不能用了:

class Example {
    fun String.method2(i: Int) {
        ...
    }
    ...
    "rengwuxian".method2(1)// 可以调试
}

"rengwuxian".method2(1)//类的外部不能调用

这个……也好理解吧?你为什么要把扩展函数写在类的里面?不就是为了让他不要被外界看到造成污染吗,是吧?

指向扩展函数的引用

函数时可以使用双冒号被指向的:

Int::toFloat

其实指向的并不是函数本身,而是和函数等价的一个对象,这也是为什么你可以对这个引用调用invoke(),却不能对函数本身调用:

(Int::toFloat)(1)// 等价于1.toFloat()
Int::toFloat.invoke(1)// 等价于 1.toFloat()
1.toFloat().invoke()// 报错

但是为了简单起见,我们通常可以把这个「指向和函数等价的对象的引用」称作是「指向这个函数的引用」,这个问题不大。那么我们基于这个叫法继续说。 普通函数可以被指向,扩展函数同样也是可以被指向的:

fun String.method1(i: Int) {

}
...
String::method1

不过如果这个扩展函数不是Top-Level的,也就是说如果它是某个类的成员函数,它就不能被引用了:

class Extensions {
    fun String.method1(i: Int) {
        ...
    }
    ...
    String::method1//报错
}

为什么?你想啊,一个成员函数怎么引用:类名加双冒号加函数名对吧?扩展函数呢?也是类名加双冒号加函数名对吧?只不过这次是Receiver的类名。那成员扩展函数呢?还用类名加双冒号加函数名呗?但是……用谁的类名?是这个函数所属的类名,还是它的Receiver的类名?这是有歧义的,所以Kotlin就干脆不许我们引用既是成员函数又是扩展函数的函数了,一了百了。

同样是成员函数的引用一样,扩展函数的引用也可以被调用,直接调用或者用invoke()都可以,不过要记得把Receiver也就是接收者或者说调用者填成第一个参数:

(String::method1)("rengwuxian", 1)
String::method1.invoke("rengwuxian", 1)

// 以上两句都等价于
"rengwuxian".method1(1)

把扩展函数的引用赋值给变量

同样的,扩展函数的引用也可以赋值给变量:

val a: String.(Int) -> Unit = String::method1

然后你再拿着这个变量调用,或者再次传递给别的变量,都是可以的:

"rengwuxian".a(1)
a("rengwuxian", 1)
a.invoke("rengwuxian", 1)

有无Receiver的变量的互换

另外大家可能会发现,当你拿着一个函数的引用去调用的时候,不管是一个普通的成员函数还是扩展函数,你都需要把Receiver也就是接受者或者调用者作为第一个参数填进去。

(String::method1)("rengwuxian", 1)// 等价于 "rengwuxian".method1(1)
(Int::toFloat)(1)// 等价于1.toFloat()

为什么?因为你拿到的是函数引用而不是调用者的对象,所以没办法在左边写上调用者啊,是吧? 所以Kotlin想要支持让我们拿着函数的引用去调用,就必须给个途径让我们提供调用者。那提供怎样的途径呢?最终Kotlin给我们的方案就是:在这种调用方式下,增加一个函数参数,让我们把第一个参数的位置填上调用者。这样,我们就可以用函数的引用来调用成员逆函数和扩展函数。但同时,又有一个问题我不知道你们发现没有: 既然有Receiver的函数可以以无Receiver的方式来调用,那……它可以赋值给无Receiver的函数类型的变量吗?

val b: (String, Int) -> Unit = String::mmethod1// 这样可以吗?

答案是,可以的。在Kotlin里,每一个有Receiver的函数——其实就是成员函数和扩展函数——它的引用都可以赋值给两种不同的函数类型变量:一种是有Receiver的, 一种是没有Receiver的:

val a String.(Int) -> Unit = String::method1
val b (String, Int) -> Unit = String::method1

这两种写法都是合法。为什么?因为有用啊,是吧?

而且同样的,这两种类型的变量也可以互相赋值来进行转换:

val a: String.(Int) -> Unit = String::method1
val b: (String, Int) -> Unit = String::method1
val c: String.(Int) -> Unit = b
val d: (String, Int) -> Unit = a

懵了?蒙就对了,不要急,继续看,只是掌握住了,下去慢慢试着琢磨。

继续

既然这两种类型的变量可以互相赋值来转换,那不就是说无Receiver得函数引用也可以赋值给有Receiver的变量? 这样的话,是不是一个普通的无Receiver的函数也可以直接赋值给有Receiver的变量?

fun method3(s: String, i: Int) {
    
}
...
val e: (String, Int) -> Unit = ::method3
val f: String.(Int) -> Unit = ::method3//这种写法也行

哇塞,没有报错! 是的,这样赋值也是可以的。 通过这些类型的互相转化,你可以把一个本来没有Receiver的函数变得可以通过Receiver来调用:

fun method3(s: String, i: Int) {

}
...
val f: String.(Int) -> Unit = ::method3
"rengwuxian".method3(1)// 不允许调用,报错
"rengwuuxian".f(1)// 可以调用

这就很爽了哈? 当然了你也可以反向操作,去把一个有Receiver的函数变得不能用Receiver调用:

fun String.method1(i: Int) {

}
...
val b: (String, Int) -> Unit = String::method1
"rengwuxian".mmethod(1)// 可以调用
"rengwuxian".b(1)// 不允许调用,报错

这样收窄功能好像没什么用哈?不过我还是要把这个告诉你,因为这样你的知识体系才是完整的。

扩展属性

除了扩展函数,Kotlin的扩展还包括扩展属性。它跟扩展函数试试一个逻辑,就是在声明的双属性左边写上类名加点,这就是一个扩展属性,英文原名叫Extension Property。

val Float.dp
    get() = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        this,
        Resources.getSystem().displayMetrics
    )

...
val RADIUS = 200f.dp

它的用法和扩展函数一样,但少了扩展函数在引用上以及Receiver上的一些比较绕的问题,所以很简单, 你自己去研究去吧。有些东西协程扩展属性是比扩展函数要更加直观和方便的,所以虽然它很简单,但研究一下绝对有函数。

总结

这次讲的内容挺多的,但其实也很简单,主要就这么几点:扩展函数、扩展函数的引用、有无Receiver的函数类型的转换以及扩展属性。

自己的补充——扩展属性

在Kotlin中,扩展属性(Extension Properties)是一种允许我们向现有类添加属性的机制。它们允许我们在不修改类定义的情况下,为类添加额外的属性。扩展属性的原理其实是利用Kotlin的扩展函数机制。扩展函数允许我们向现有类添加新的函数。扩展属性则是在这个基础上进一步扩展,使得我们可以向类中添加新的属性。

下面是一个例子:假设我们有一个User类,它具有name和age两个属性:

class User(val: name: String, val age: Int)

现在,我们想要为User添加一个扩展属性isAdult,用于判断用户是否成年。我们可以使用扩展属性来实现这个功能:

val User.isAdult: Boolean
    get() {
        return age >= 18
    }

在上面的代码中,我们通过使用val关键字定义了一个扩展属性isAdult,它返回一个布尔值。在get()方法中,我们根据用户的年龄来判断用户是否成年。现在我们可以使用这个扩展属性,就像访问普通属性一样:

fun main() {
    val user = User("Alice", 25)
    println(user.isAdult) // 输出true
    
    val user2 = User("Bob", 16)
    println(user.isAdult)// 输出false
}

在上面的例子中,我们创建了两个User对象,并通过isAdult扩展属性来判断他们是否成年。输出结果分别为true和false。 通过这个例子,我们可以看到,扩展属性使得我们能够为现有类添加的额的属性,而无需修改类的定义。这为我们不改变现有类的情况下,为类添加新的功能提供了一种编写的方式。

扩展函数可以调用被扩展类里面的属性和方法吗?

扩展函数可以调用被扩展类里面的属性

为什么呢? 实际上,尽管Kotlin的扩展函数在语法上看起来像是被扩展类的成员函数,但在编译器内部,它们实际上被转换为顶层函数。这是Kotlin在编译器所提供的一种语法糖。

当我们定义一个扩展函数时,编译器会将函数编译为一个静态的顶层函数,并使用被扩展的类作为第一个参数(称为Receiver)。这个接收者类型参数对于我们在扩展函数中使用的this关键字。

因此,当我们在扩展函数内部访问被扩展类的属性时,实际上是通过该接受者类型参数来访问的。编译器会自动将调用该扩展函数时的实际接受者对象传递给该参数。

这样的设计使得扩展函数可以像成员函数一样访问被扩展类的属性和方法,而不需要显式地传递接收者对象。这使得代码更加简单,让扩展函数的使用更加方便。

下面是一个简化的示例来是说明这个原理:

fun String.printlength() {
    println("Length of the string:${this.length}")
}

再编译器内部,它会被转换为一下形式的顶层函数:

fun printLength(str: String) {
    println("Length of the string:${str.length}")
}

这样,我们调用message.printLength()实际上是调用了printLength(message)

总结起来,尽管Kotlin的扩展函数看起来像是被扩展类的成员函数,但它们实际上是编译器转换的顶层函数。编译器使用被扩展类作为第一个参数,并通过这个参数来访问本扩展类的属性和方法。这样的设计让我们在使用扩展函数时可以像调用成员函数一样自然地访问被扩展类的成员。

版权声明

本文首发于:会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions Properties)

微信公众号:扔物线