Kotlin 扩展函数

1. 为什么需要扩展函数?

我们都知道 Koltin 这门语言与 Java 有非常好的互操作性,并且扩展函数这个新特性可以很平滑与现有Java 代码集成。甚至纯 Kotlin 的项目都可以基于 Java 库或者 Android 中的一些第三方框架库来构建,所以扩展函数非常适合 Kotlin 和 Java 语言混合开发模式。在很多公司一些比较稳定良好的库都是 Java 开发的,也完全没必要去用 Kotlin 语言重写。但是想要扩展库的接口和功能,这时候扩展函数可能就会派上用场。使用 Kotlin 的扩展函数还有一个好处就是没有副作用,不会对原有库代码或功能产生影响

先来一个扩展函数的例子一睹为快,来实现一个给 Android 中 TextView 组件设置加粗的扩展函数:

//扩展函数的定义
fun TextView.isBold() = this.apply { 
    paint.isFakeBoldText = true
}

//扩展函数的调用
activity.find<TextView>(R.id.course_comment_tv_score).isBold()

2. 扩展函数的语法

上面例子不难看出,Kotlin 的扩展函数是真的强大,可以毫无副作用给原有库的类增加属性和方法,比如例子中 TextView,我们根本没有去动 TextView 源码,但是却给它增加一个扩展函数。具有那么强大功能,到底它背后原理是什么呢?下面我们来深入学习下它的语法以及语法背后的原理。

2.1 扩展函数的定义

Kotlin 可以为一个不能修改的或来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用,这种机制的函数称为扩展函数

//扩展函数定义
fun TextView.isBold() = this.apply { 
    paint.isFakeBoldText = true
}

2.2 扩展函数的分析

扩展函数只需要把扩展的类或者接口名称,放到即将要添加的函数名前面。这个类或者名称就叫做接收者类型,类的名称与函数之间用 . 调用连接。this指代的就是接收者对象,它可以访问扩展的这个类可访问的函数或属性。

图片描述

2.3 扩展函数的背后原理

扩展函数实际上就是一个对应 Java 中的静态函数,这个静态函数参数为接收者类型的对象,然后利用这个对象就可以访问这个类中的成员属性和方法了,并且最后返回一个这个接收者类型对象本身。这样在外部感觉和使用类的成员函数是一样的:

//这个类名就是顶层文件名+“Kt”后缀
public final class ExtendsionTextViewKt {
   @NotNull
   public static final TextView isBold(@NotNull TextView $receiver) {//扩展函数isBold对应实际上是Java中的静态函数,并且传入一个接收者类型对象作为参数
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.getPaint().setFakeBoldText(true);//设置加粗
      return $receiver;//最后返回这个接收者对象自身,以致于我们在Kotlin中完全可以使用this替代接收者对象或者直接不写。
   }
}

3. 扩展函数的使用场景

扩展函数的使用场景主要就是毫无副作用给原有库的类增加函数,增强类的行为。比如 Android 中给所有 View 组件增加常见动画、给 ImageView 组件增加一个加载网络图片函数、给SpannableStringBuilder 增加 bold 加粗、italic 斜体、color 颜色等一系列 Span 的设置以及很多需要重复设置模板代码场景都可以使用扩展函数。

4. 扩展函数的使用示例

示例1: Android 中给 ImageView 组件增加一个 loadUrl 的扩展函数,不再需要繁琐 Glide 的调用,只需简单一行 loadUrl 代码的调用就能实现加载图片功能:

//loadUrl扩展函数接收者类型是ImageView
fun ImageView.loadUrl(requestManager: RequestManager = Glide.with(context)
                      , url: String = ""
                      , urls: List<String> = listOf(url)
) {
            requestManager.view(this).url(urls).start()//this指代的是ImageView这个接收者对象实例, 这里this也可以省略
}

//使用扩展函数来加载多张图片的调用方式
course_cover_iv1.loadUrl("https://www.imooc.com/test1.png")//imageView直接调用loadUrl像是给ImageView增加一个网络图片加载函数一样,调用非常简单。
course_cover_iv2.loadUrl("https://www.imooc.com/test2.png")
course_cover_iv3.loadUrl("https://www.imooc.com/test3.png")

//不使用扩展函数来加载多张图片的调用方式
Glide.with(context)
     .view(course_cover_iv1)
     .url("https://www.imooc.com/test1.png")
     .start()

Glide.with(context)
     .view(course_cover_iv2)
     .url("https://www.imooc.com/test2.png")
     .start()

Glide.with(context)
     .view(course_cover_iv3)
     .url("https://www.imooc.com/test3.png")
     .start()     

示例2: Android 中给 View 组件增加一个 animAlpha 透明动画的扩展函数,不需要每次都去重新写属性动画的配置,只需要简单一行代码调用 View 中的方法即可。像是给原本就不具备透明动画 View 组件无缝地提供一个动画的功能:

//animAlpha扩展函数接收者类型是View,也就是View的子类都可以直接使用animAlpha函数
fun View.animAlpha(
       duration: Long = 600,
       alpha: Float = 0f, 
       delay: Long = 0, 
       animatorEnd: ((Animator?) -> Unit)? = null
 ) {
        this.animate()//this指代的是View这个接收者对象实例, 这里this也可以省略
            .setDuration(duration)
            .setInterpolator(AccelerateDecelerateInterpolator())
            .setStartDelay(delay)
            .alpha(alpha)
            .setListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {

                }

                override fun onAnimationEnd(animation: Animator?) {
                    animatorEnd?.invoke(animation)
                }

                override fun onAnimationCancel(animation: Animator?) {

                }

                override fun onAnimationStart(animation: Animator?) {

                }
            })
            .start()
}

//animAlphaVisible扩展函数,设置View组件的显示和消失透明度动画,通过isVisible控制
fun View.animAlphaVisible(
       isVisible: Boolean, 
       duration: Long = 600, 
       animatorEnd: ((Animator?) -> Unit)? = null
) {
    //扩展函数内部再调用扩展函数animAlpha
    animAlpha(duration, alpha = if (isVisible) 1f else 0f, animatorEnd = animatorEnd)
}

//扩展函数animAlpha调用
mEditText.animAlpha(alpha = 0.2f)//EditText组件通过最开始透明度变化到alpha为0.2f

//扩展函数animAlphaVisible调用
mIv.animAlphaVisible(isVisible = true)//ImageView组件设置透明度显示的动画
mTv.animAlphaVisible(isVisible = false)//TextView组件设置透明度消失的动画

5. 扩展函数需要注意的点

  • 扩展函数不能像成员函数那样被子类重写;

  • 扩展函数不能访问原始类中私有成员属性或成员函数;

  • 扩展函数的本质通过传入对象实例,委托对象来访问成员函数,所以它的访问权限和对象访问权限一致。

6. 扩展函数与成员函数的区别

我们通过上述扩展函数的学习都知道,扩展函数在外部调用方式来看和类的成员函数是一致,但是两者却有着本质的区别。

  • 扩展函数和成员函数使用方式类似,可以直接访问被扩展类的方法和属性。(原理: 传入了一个扩展类的对象,内部实际上是用实例对象去访问扩展类的方法和属性);
  • 扩展函数不能打破扩展类的封装性,不能像成员函数一样直接访问内部私有函数和属性。(原理: 原理很简单,扩展函数访问实际是类的对象访问,由于类的对象实例不能访问内部私有函数和属性,自然扩展函数也就不能访问内部私有函数和属性了);
  • 扩展函数实际上是一个静态函数是处于类的外部,而成员函数则是类的内部函数;
  • 父类成员函数可以被子类重写,而扩展函数则不行。

7. 扩展函数使用经验

虽然扩展函数功能十分强大,但是记住千万不要滥用扩展函数,并不是所有场景定义都合适定义成扩展函数。定义成扩展函数需要抓住以下三个点:

  • 第一,这个函数需要被很多地方复用,定义成扩展函数减少很多重复模板代码;

  • 第二,这个函数具有扩展的原始类,如果没有那么就不适合定义成扩展函数,它经常很容易会和顶层函数使用场景弄混;

  • 第三,如果需要扩展一个原始类的行为时候用扩展函数,如果是扩展一个原始类的状态的时候建议使用扩展属性。

8. 总结

扩展函数的本质是一个对应 Java 中的静态函数,这个静态函数参数为接收者类型的对象,然后利用这个对象就可以访问这个类中的成员属性和方法了,并且最后返回一个这个接收者类型对象本身。这样在外部感觉和使用类的成员函数是一样的。另外,扩展函数与成员函数的相同点和不同点也是扩展函数中的重点。