开端

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():
    「码上开学——hencoder」Kotlin笔记(会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions )
    只需引证了对应的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)

微信公众号:扔物线