kotlin扩展函数详解

0 阅读5分钟

kotlin支持在不修改原有类的情况下增加新的函数,增加的函数就像原本类中就有这个函数一样。

扩展函数

例如之前有一个Person类,可以通过getName()获取中文名,现在想增加一个获取英文名称的函数,就通过扩展函数实现。可以在不修改原有类的情况下,增加功能。

class Person {

    fun getName(): String {
        return "张三"
    }
}

扩展函数的写法如下:

/**
 * fun 类名.函数名(){
 *
 * }
 */
fun Person.getEnglishName(): String {
    return "ZhangSan"
}

通过 类名.函数名就为该类添加了一个扩展函数。 这个类就多了一个函数,扩展函数的调用方式和普通函数一样。实例对象.函数()就行了。

class Person {

    fun getName(): String {
        return "张三"
    }
}

/**
 * fun 类名.函数名(){
 *
 * }
 */
fun Person.getEnglishName(): String {
    return "ZhangSan"
}


fun main() {
    val person = Person()
    person.getName()
    person.getEnglishName()
}

扩展函数的实现原理

将上面的kotlin代码反编译java代码,可以看到kotlin的扩展函数,在java层面实际上就是一个static函数,只不过这个函数接收了一个实例对象。扩展函数本质上是通过把实例对象传入到函数里面,来实现对类功能的扩展。

扩展函数需要接收一个实例,所以kotlin被扩展的类,也就是要作为参数传入实例称为receiver

类名.函数名()中的类名就叫receiver

public final class Person {

    public final String getName() {
        return "张三";
    }
}

public final class PersonKt {

    // 扩展函数对应的java方法是一个static方法
    // 将Parent作为参数传进来
    public static final String getEnglishName(@NotNull Person $this$getEnglishName) {
        Intrinsics.checkNotNullParameter($this$getEnglishName, "<this>");
        return "ZhangSan";
    }

    public static final void main() {
        Person person = new Person();
        person.getName();
        // 调用static方法,然后把person传入其中
        getEnglishName(person);
    }

    public static void main(String[] args) {
        main();
    }
}

上面的扩展函数都是写在类外面的,也是就顶级函数,直接定义在文件顶层,不属于任何类。扩展函数其实也可以写在类里面,如下:

class Person {

    fun getName(): String {
        return "张三"
    }

    fun Person.getEnglishName2(): String {
        return "ZhangSan_V2"
    }
}

写在了类里面就没法像调用成员函数一样调用了,没办法像调用getEnglishName一样调用getEnglishName2

因为getEnglishName2就不是一个static方法了,在java层面就是一个成员函数。反编译的java代码如下:

public final class Person {

    public final String getName() {
        return "张三";
    }

    // 非static方法
    public final String getEnglishName2(@NotNull Person $this$getEnglishName2) {
        Intrinsics.checkNotNullParameter($this$getEnglishName2, "<this>");
        return "ZhangSan_V2";
    }
}

按照反编译的java代码来看,想要调用getEnglishName2就需要先创建Person的实例对象,再把用这个实例调用getEnglishName2并作为参数传入。

但是很可惜kotlin不支持这种写法。

在有T.()或者Person.()作为入参的方法中才能调用。

因为apply是一个inline函数,inline函数在编译时会在编译时把代码直接插入到调用的地方,所以反编译的java代码,没有apply方法里面的东西,看不出来T.()是什么。因此复制apply的代码重新写一个apply2,只删除inline关键字。代码如下:

// kotlin源码
fun main() {
    val person = Person()
    person.getName()
    person.getEnglishName()

    person.apply2 {
        getEnglishName2()
    }
}

fun <T> T.apply2(block: T.() -> Unit): T {
    block()
    return this
}

// 反编译的java代码
public final class PersonKt {

    public static final void main() {
        Person person = new Person();
        person.getName();
        getEnglishName(person);
        
        apply2(person, PersonKt::main$lambda$0);
    }

    public static final Object apply2(Object object, @NotNull Function1 block) {
        // 间接调用main$lambda$0()并传入person实例
        block.invoke(object);
        return object;
    }

    private static final Unit main$lambda$0(Person person) {
        // 调用扩展函数
        person.getEnglishName2(person);
        return Unit.INSTANCE;
    }
}

从反编译的代码可以看出,T.() -> Unit对应的Function1类。T.()就是Function1.invoke()的入参。所以kotlin也把T.()中的T称为Receiver。

kotlin通过Function1来实现了扩展函数的调用。

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

小结:在类中定义的扩展函数,需要在该类的扩展函数的实现中才能调用。例如Person类中定义的扩展函数,就需要在Person.() -> Any的实现中调用。

扩展函数本质上就是将receiver作为参数传到函数中。Persson.getEnglishName()本质上就是getEnglishName(persion)

实际案例

Compose中的Modifier很多地方都用到了写在类中的扩展函数。

interface ColumnScope {

    // 定义在接口中的扩展函数
    // 对应的java代码是 weight(modifier,weight,fill)
    fun Modifier.weight(
        weight: Float,
        fill: Boolean = true
    ): Modifier
}

interface Modifier {
    companion object : Modifier {
        override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
        override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
        override fun any(predicate: (Element) -> Boolean): Boolean = false
        override fun all(predicate: (Element) -> Boolean): Boolean = true
        override infix fun then(other: Modifier): Modifier = other
        override fun toString() = "Modifier"
    }
}

weight定义在了ColumnScope接口中,想要调用weight就需要先创建一个ColumnScope的实例,再把一个Modifier的实例传入到weight(moidifier)

所以只有在ColumnScope.() -> Unit的实现中才能调用到weight()。在外部调用就会找不到weight函数,因为外部没有ColumnScope的实例。

    Column() {
        /**
         * 对应的java代码:
         * columnScope.weight(Modifier,1f)
         */
        Modifier.weight(1f)
    }

扩展属性

除了函数可以扩展,属性也可以扩展。

扩展属性写法:Val 类名.属性名

/**
 * 扩展属性
 */
val Person.chineseName: String
    get() = "张三"

/**
 * 扩展函数
 */
fun Person.getEnglishName(): String {
    return "ZhangSan"
}

调用方式和普通的成员变量一样。如果是val只读变量,扩展属性本质上和扩展函数在java层面没有任何区别,都是生成了一个static方法。

public final class PersonKt {
   // 扩展属性
   // 对应kotlin的 val Person.chineseName
   public static final String getChineseName(@NotNull Person $this$chineseName) {
      Intrinsics.checkNotNullParameter($this$chineseName, "<this>");
      return "张三";
   }

   // 扩展函数
   // 对应 fun Person.getEnglishName()
   public static final String getEnglishName(@NotNull Person $this$getEnglishName) {
      Intrinsics.checkNotNullParameter($this$getEnglishName, "<this>");
      return "ZhangSan";
   }
}

var可变属性会多生成一个set方法。

public final class PersonKt {

    // var Person.chineseName
   public static final String getChineseName(@NotNull Person $this$chineseName) {
      Intrinsics.checkNotNullParameter($this$chineseName, "<this>");
      return "张三";
   }

    // var Person.chineseName
   public static final void setChineseName(@NotNull Person $this$chineseName, @NotNull String value) {
      Intrinsics.checkNotNullParameter($this$chineseName, "<this>");
      Intrinsics.checkNotNullParameter(value, "value");
   }
}

和扩展函数一样,扩展属性除了能写在类外面,还可以写在类里面。在java代码层面,扩展属性和扩展函数的基本额没有区别。