【Kotlin回顾】4.扩展

139 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

1.什么是扩展?

扩展就是在原有功能的基础上增加新的的功能,Kotlin的扩展就是在原有的类基础上增加新的方法或新的属性,它分为扩展函数和扩展属性两种。

2.为什么要有扩展?

软件开发中会遇到接入第三方SDK的情况,此时需要一个依托于这个SDK的功能但是这个第三方SDK并没有提供,那就需要扩展了,这个在Java中是无法实现的,在Kotlin中可以实现。

3.扩展函数

扩展函数就是从类的外部扩展出来的一个函数,它看起来就像是类中的成员函数一样,但是这个类中的成员函数并不能调用它:代码如下:

fun main() {
    val msg = "Hello Kotlin"

    // lastElement就像String的成员方法一样可以直接调用
    val last = msg.lastElement()

    // 输出结果:last:n
    println("last:$last")
    
    
    val time = 1218163328000L
    //输出结果today:2008年08月08
    println("today:${time.today()}")
}

//lastElement是String类中的扩展函数
//获取字符串的最后一个字符
fun String.lastElement(): Char? {
//    if (this.isEmpty()) {     //this可以省略
    if (isEmpty()) {
        return null
    }
    return this[length - 1]
}

//today是Long类型中的扩展函数
//根据一个时间戳(Long类型)获取年月日
fun Long.today(): String {
    return SimpleDateFormat("yyyy年MM月dd").format(this)
}

从示例代码中有以下几个信息要关注:

    • 方法的命名还是用fun为关键字;
    • lastElement方法中的String.代表扩展函数是为String这个类定义的,同理Long.也是为Long这个类定义的。在Kotlin中这个叫做接收者,也就是扩展函数的接收方;
    • lastElementtoday是扩展函数的函数名称。
    • Char?String是返回值的类型,这根普通函数没什么区别;
    • lastElement函数中this.isEmpty()中的this可以省略,因为这个this是一个作用于,就是上面定义的msg。当然在today方法中的format(this)中的this是不能省略的,否则format不知道数据源是哪一个。

总结:扩展函数就是普通函数的前面加上【接收者.】 即可,至于具体的用法就需要在实际编码过程中根据功能需要自行定义了。

4.扩展函数的原理

public final class ExtensionDemoKt {
   public static final void main() {
      long time = 1218163328000L;
      String var2 = "today:" + today(time);
      boolean var3 = false;
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   @NotNull
   public static final String today(long $this$today) {
      String var10000 = (new SimpleDateFormat("yyyy年MM月dd")).format($this$today);
      Intrinsics.checkNotNullExpressionValue(var10000, "SimpleDateFormat("yyyy年MM月dd").format(this)");
      return var10000;
   }
}


//整理一下
public class ExtensionDemoKt {
    public static void main(String[] var0) {
        long time = 1218163328000L;
        System.out.println("today:" + today(time));
    }

    @NotNull
    public static final String today(long $this$today) {
        String var10000 = (new SimpleDateFormat("yyyy年MM月dd")).format($this$today);
        return var10000;
    }
}

上面的代码是将示例代码转换成Java代码,从代码中可以获取到以下几个信息:

    • 定义的Long.today扩展函数转换成Java后,today方法就会成为一个静态方法,这个静态方法在其他方法调用是就是这样的ExtensionDemoKt.today(1218163328000L);
    • 原本没有传参的today方法在转换成Java代码后多了一个参数,参数来源就是调用前传递的;
    • JVM不理解Kotlin的扩展语法,因此Kotlin编译器将扩展函数转换成静态方法,扩展函数的调用处的代码也就转换成了静态方法的调用。

5.扩展属性

扩展函数时在类的外部为它定义一个新的成员方法,扩展属性是在类的外部为它定义了一个新的成员属性

通过扩展函数知道了定义一个扩展函数是需要加上一个接收者,扩展属性也是如此,同样的扩展函数的定义只是在语法层面的,扩展属性也是如此,看一下扩展属性的代码,通过扩展属性的方式实现today

fun main() {
    val time = 1218163328000L
    println("today:${time.today}")

}

val Long.today: String
    get() =
        SimpleDateFormat("yyyy年MM月dd").format(this)

扩展属性代码转Java

public final class ExtensionDemoKt {
   public static final void main() {
      long time = 1218163328000L;
      String var2 = "today:" + getToday(time);
      boolean var3 = false;
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   @NotNull
   public static final String getToday(long $this$today) {
      String var10000 = (new SimpleDateFormat("yyyy年MM月dd")).format($this$today);
      Intrinsics.checkNotNullExpressionValue(var10000, "SimpleDateFormat("yyyy年MM月dd").format(this)");
      return var10000;
   }
}

//整理一下
public class ExtensionDemoKt {

    public static void main(String[] var0) {
        long time = 1218163328000L;
        String var2 = "today:" + getToday(time);
        boolean var3 = false;
        System.out.println(var2);
    }

    @NotNull
    public static final String getToday(long $this$today) {
        String var10000 = (new SimpleDateFormat("yyyy年MM月dd")).format($this$today);
        return var10000;
    }
}

可以看到除了today方法名变为getToday之外其他几乎跟扩展函数的today一模一样,而这个getToday也是因为自定义的get才出现的。

总结:Kotlin的扩展表面上看起来是为了一个类扩展了新的成员,但是实际上它还是一个静态方法。并且不管是扩展函数还是扩展属性最终都会变成一个静态方法,这两个扩展具体该怎么用就要取决于具体场景了,要看这个场景更适合函数还是属性。

6.扩展能做什么

普通类、单例类、密封类、伴生类,以及第三方提供的Java类都可以被扩展,匿名内部类除外因为它没有名称无法指定接收者,也没必要扩展。

7.扩展有哪些限制

①.扩展不是真正的类成员不能被其他子类重写。

下面的代码定义了一个父类且父类和其内部方法被open修饰,子类可以继承父类也可以重写父类中的方法,但是父类还定义了一个扩展属性和扩展函数,而子类却不能引用父类的属性和重写父类扩展的方法,因为自定义的这个属性和函数不属于父类HomeAppliances,Kotlin编译器会将其编译成Java的静态方法,所以只能调用不能重写。

/**
 * 定义一个可继承的父类:电器
 */
open class HomeAppliances {
    var other: String = ""

    open fun characteristic() {
        println("需要电才能启动")
    }
}

/**
 * 扩展一个属性:可以制冷
 */
val HomeAppliances.coolAir: Boolean
    get() = other == "制冷"

/**
 * 扩展一个函数:用遥控器启动
 */
fun HomeAppliances.start() {
    println("启动")
}

/**
 * 定义一个电器的子类:空调
 */
class AirConditioner : HomeAppliances() {
    override fun characteristic() {
        super.characteristic()
        println("我是空调")
    }
}

②.扩展属性不能存储状态

coolAir的扩展属性中它的值是由other决定的,other的值是由外部传入,它本身是没有任何状态的也无法存储状态,因为被反编译后最中都会是一个静态方法。

③.扩展作用域有限

扩展的作用域仅限于两个地方:①:定义处的成员;②:接收者类型的公开成员。

//ExtensionDemo.kt
private const val msg: String = ""

fun String.lastElement(): Char? {
    if (this.isEmpty()) {
        println(msg)
        return null
    }

    return this[length - 1]
}
  • 扩展作用域——定义处的成员:以上内容都定义在同一个文件中,因此即使msg变量是private声明的,在扩展方法lastElement依旧可以被访问;
  • 扩展作用域——接收者类型的公开成员:再看最后一行return this中的length这是一个变量,它属于String类并且使用public声明的,同样的String类中其他用public声明的函数或者变量也都可以使用,这就是接收者类型的公开成员,如果是其他修饰符声明的就不可以被使用,说到底还是因为扩展函数并不是真正的类成员。

问题:如果将扩展定义在某个类的内部,它能够访问这个类的私有属性吗?

可以访问,但是存在问题,将扩展定义在类的内部反编译成Java后会发现定义的扩展函数、扩展属性都成为了这个类的内部类或者说成为了这个类的内部成员函数和成员属性并且不是静态的,也就是说其他外部类不能直接访问它。

总结:

  • 如果扩展是顶层扩展,那么该扩展的作用域仅限于该Kotlin文件当中的所有成员以及被扩展类型的公开成员,这种方式定义的扩展是可以被全局使用的。
  • 如果扩展被定义在某个类当中,那么该扩展的作用于仅限于这个类中的所有成员以及被扩展类型(接收者)的公开成员,这种方式定义的扩展只能在这个类中使用其他类访问不到。

8.使用场景:

  • 主动使用扩展通过它来优化软件架构。 对复杂的类进行职责划分让关注点分离。是类的核心尽量简单易懂,而让类的功能属性与方法以扩展的形式存在与类的外部,例如String.ktStrings.kt
  • 被动使用扩展,提升可读性与开发效率。 对于无法修改的第三方SDK或者重复的代码可以用扩展的方式将其封装提供给对应的接收者类型。例如第一段代码中的Long.today开发中可能会遇到多种时间格式,可以将其封装然后调用,第一段代码可以这么改
fun main() {
    println("today:${System.currentTimeMillis().today()}")
    println("todayAndTime:${System.currentTimeMillis().todayAndTime()}")
}

fun Long.today(): String {
    return SimpleDateFormat("yyyy年MM月dd").format(this)
}

fun Long.todayAndTime(): String {
    return SimpleDateFormat("yyyy年MM月dd HH:mm:ss").format(this)
}

9.思考题

Kotlin 的扩展是允许我们为“可为空的类型”进行扩展的。下面的代码有何区别

// 不为空的接收者类型
//     ↓
fun String.lastElement(): Char? {}

// 可为空的接收者类型
//     ↓
fun String?.lastElement(): Char? {}

解答:接收者不可为空的不可以用等于null的值调用,接收者可为空的可以被等于null的值调用。