Kotlin | 扩展函数(终于知道为什么 with 用 this,let 用 it)

简介: Kotlin | 扩展函数(终于知道为什么 with 用 this,let 用 it)

前言


扩展是 Kotlin 的一种语言特性,即:在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性。扩展使我们可以合理地遵循开闭原则,在大多数情况下是比继承更好的选择。


目录

image.png

前置知识


这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


1. 为什么要使用扩展?


在 Java 中,我们习惯于把通用代码封装到工具类中,诸如 StringUtils、ViewUtils 等,例如:


StringUtils.java


public static void firstChar(String str) {
    ...
}
复制代码


在使用时,我们就需要调用StringUtils.firstChar(str)。然而,这种传统的调用方式不够简单直接,表意性也不够强,会让调用方忽略 String 和 firstChar() 间的强联系。另外,调用方也希望省略 StringUtils 类名,让 firstChar() 看起来更像是 String 内部的一个属性和方法,像这样:"str".firstChar()


要实现这种方式,在 Java 中就需要修改或继承 String 类,然而 String 是 JDK 中的 final 类,不能修改或继承。


这个时候可以使用 Kotlin 扩展来解决这个问题,我们可以把 firstChar 定义为 String 的扩展函数:


StringUtils.kt


定义 String 的扩展函数
fun String.firstChar() {
    ...
}
复制代码


此时,在使用时可以采用"str".firstChar()的方式。在这里我们扩展了 String 类,却没有修改或继承 String。


总结:扩展是 Kotlin 中的一种特性,可以 在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性,更符合开闭原则。


开闭原则(OCP,Open Closed Principle)

开闭原则是面向对象软件设计的原则之一,即:对扩展开放,而对修改是封闭的。


2. 扩展函数 & 扩展属性


2.1 声明扩展


声明扩展非常简单,只需要在声明时增加「类或者接口名」。这个类的名称称为 接收者类型(receiver type),调用这个扩展的对象称为 接收者对象。大多数情况下,扩展会声明为「顶级成员」,例如:


Utils.kt


声明扩展函数:
fun <T : Any?> MutableList<T>.exchange(fromIndex: Int, toIndex: Int) {
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}
声明扩展属性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0
复制代码


在使用时,就可以直接像使用普通成员函数 / 属性一样:

xxx.kt


val list = mutableListOf(1,2,3)
使用扩展函数:
list.exchange(1,2)
使用扩展属性:
val isEven = list.sumIsEven
复制代码

提示: MutableList 是接收者类型,list 是接收者对象。


在扩展函数内部,你可以像 「成员函数」 那样使用this来引用接受者对象,当然有时也可以省略,例如:


声明扩展属性:
val MutableList<Int>.sumIsEven
    get() = this.sum() % 2 == 0 // 省略了 this.sum() 中的 this
复制代码

2.2 可空接收者


第 2.1 节 中使用了「非空的接收者类型」来定义扩展(MutableList 没有关键词?),当使用「可空变量」调用扩展时,会报编译时错误。例如:


val list:MutableList<Int>? = null
list.sumIsEven // Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type MutableList<Int>?
复制代码


根据提示,我们知道可以 使用「可空的接收者类型」来定义扩展,同时还要在内部使用null == this来对接收者对象进行判空。例如:


可空接收者类型的扩展函数
fun <T : Any?> MutableList<T>?.exchange(fromIndex: Int, toIndex: Int) {
    if (null == this) return
    val temp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = temp
}
可空接收者类型的扩展属性
val MutableList<Int>?.sumIsEven: Boolean
    get() = if (null == this)
        false
    else
        this.sum() % 2 == 0
复制代码

2.3 在 Java 中调用


扩展的本质:扩展函数是定义在类外部的静态函数,函数的第一个参数是接收者类型的对象。这意味着调用扩展时不会创建适配对象或者任何运行时的额外消耗。


在 Java 中,我们只需要像调用普通静态方法那样调用扩展即可。例如:


xxx.java


ArrayList<Integer> list = new ArrayList<>(3);
使用扩展函数:
UtilsKt.exchange(list, 1, 2);
使用扩展属性:
boolean isEven = UtilsKt.getSumIsEven(list);
复制代码

2.4 扩展的作用域


当你定义了一个扩展之后,它不会自动在整个项目内生效。在其它包路径下,需要使用improt导入。例如:


import Utils.exchange
import Utils.*
复制代码


当你在不同包中定义了 「重名扩展」,并且需要在同一个文件中去使用它们,那么你需要使用as关键字重新命名。例如:


import Utils.exchange as swap
使用时:
list.swap(0,1)
复制代码


2.5 注意事项


  • 1、扩展函数不能访问 private 或 protected 成员

扩展函数或扩展属性本质上是定义在类外部的静态方法,因此扩展不可能打破类的封装性而去调用 private 或 protected 成员;


  • 2、不能重写扩展函数

扩展函数在 Java 中会被编译为静态函数,并不是类的一部分,不具备多态性。尽管你可以给父类和子类都定义一个同名的扩展函数,看起来像是方法重写,但实际上两个函数没有任何关系。当这个函数被调用时,具体调用的函数版本取决于变量的 「静态类型」,而不是 「动态类型」


静态方法调用

关于 Java 方法调用的本质,在我之前写过的一篇文章里系统分析过:Java | 方法调用的本质(含重载与重写区别)。静态方法调用在编译后生成invokestatic 字节码指令,它的处理逻辑如下:

  • 1、编译阶段:确定方法的符号引用,并固化到字节码中方法调用指令的参数中;
  • 2、类加载解析阶段:根据符号引用中类名,在对应的类中找到简单名称与描述符相符合的方法,如果找到则将符号引用转换为直接引用;否则,按照继承关系从下往上依次在各个父类中搜索;
  • 3、调用阶段:符号引用已经转换为直接引用;调用invokestatic不需要将对象加载到操作数栈,只需要将所需要的参数入栈就可以执行invokestatic指令。


  • 3、如果类的成员函数和扩展函数拥有相同的签名,成员函数优先
  • 4、扩展属性没有支持字段,不会保存任何状态


扩展属性是没有状态的,必须定义 getter 访问器。因为不可能给现有的 Java 类添加额外的字段,所以也就没有地方可以存储支持字段。举个例子,以下代码是编译错误的:


val MutableList<Int>?.sumIsEven: Boolean = true // (X) Initializer is not allowed here because this property has no backing field
    get() = if (null == this)
        false
    else
        this.sum() % 2 == 0
复制代码



3. 标准库中的函数


在 Kotlin 标准库中,定义了一系列通用的内联函数:T.apply、T.also、T.let、T.run、with你是否清楚理解它们的用法 & 本质,它们都是扩展函数吗?


val str1: String = "".run {
    println(this.length)
    this
}
val str2: String = with("") {
    println(this.length)
    this
}
val str3: String = "".apply {
    println(this.length)
}
val str4: String = "".also {
    println(it.length)
}
val str5: String = "".let {
    println(it.length)
    it
}
复制代码


在上面的示例中,我们看到有的函数作用域内使用了this,而其它又使用了it。这两个关键字到底引用的是什么,为什么会有差别呢?


我们先找到这些函数的声明:


standard.kt


public inline fun <R> run(block: () -> R): R { 
    return block()
}
public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}
复制代码


一脸懵逼,别急,我们梳理一下:


函数 参数1 参数2 返回值
run / ()->R R
T.run / T.()->R R
with T T.()->R R
T.apply / T.()->Unit T
T.also / (T)->Unit T
T.let / (T)->R R


还是一脸懵逼,那我提几个问题:


  • runvsT.run,差了一个T,区别是什么?区别在于:run是普通函数,T.run是扩展函数。run中的this是声明的类对象(顶级函数除外),T.run中的this是接收者对象;
  • T.()->Unitvs(T)->Unit,或者T.()->Rvs(T)->R,T 的位置不同,区别是什么?区别在于:T.()->Unit中的 T 是接收者类型,(T)->Unit中的 T 是函数参数;
  • 为什么withthisletit
  • run、with、apply 函数中的参数 block 是 「T 的扩展函数」,所以采用 this 是扩展函数的接收者对象(receiver)。另外因为 block 没有参数,所以不存在 it 的定义。
  • also 和 let 参数 block 是 「参数为 T 的函数」,所以采用 it 是唯一参数(argument)。另外因为 block 不是扩展函数,所以不存在 this 的定义。


lambda 表达式

lambda 表达式本质上是 「可以作为值传递的代码块」。在老版本 Java 中,传递代码块需要使用匿名内部类实现,而使用 lambda 表达式甚至连函数声明都不需要,可以直接传递代码块作为函数值。

当 lambda 表达式只有一个参数,可以用it关键字来引用唯一的实参。


4. 扩展的应用场景


在这一节里,我们来介绍一些在 Android 开发中使用扩展的应用场景。


4.1 封装工具 Utils


在 Java 中,我们习惯于把通用代码封装到工具类中。传统 Java 的工具方法的调用方式不够简单直接,表意性也不够强,会让调用方忽略 String 和 firstChar() 间的强联系。另外,调用方也希望省略 StringUtils 类名,让 firstChar() 看起来更像是 String 内部的一个属性和方法。这些需求对 Kotlin 扩展来说都不是问题。


4.2 解决烦人的 findViewById


在 Android 中,经常会调用 findViewById() 来找到视图树中的某一个 View 实例,例如:


旧版 SDK:loginButton = (Button) findViewById(R.id.btn_login);
新版 SDK:loginButton = findViewById(R.id.btn_login);
复制代码


提示: 新版的 SDK 中,findViewById() 是一个泛型方法,所以你就不再需要进行强制类型转换。


public <T extends View> T findViewById(@IdRes int id) {
   return getWindow().findViewById(id);
}
复制代码


通常,我们会定义一个实例变量或者局部变量来承载 findViewById() 的返回值,很多时候,这些变量都只是 “临时变量” ,在进行事件绑定 / 赋值之后就没有很大的用处了。如果你面对一些比较复杂的界面,你甚至需要定义几十行临时变量!


能不能省略这些临时变量,直接操作R.id.*呢?答案是可以的,我们可以利用 Kotlin 「高阶函数 + 扩展函数」。例如:


fun Int.onClick(click: () -> Unit) {
    findViewById<View>(this).apply {
        setOnClickListener {
            click()
        }
    }
}
复制代码


此时,我们可以直接使用R.id.*来绑定点击事件(R.id* 本质就是一个整数类型):


R.id.btn_login.onClick {
    // do something
}
复制代码


这样就简洁多了,我们就不再需要定义一堆临时变量了。不过,你每次都需要写R.id前缀,这似乎也很多余,能不能再省略呢?确实可以,我们需要使用 Kotlin 为 Android 量身定制的 Gradle 插件:kotlin-android-extensions


apply plugin : 'kotlin-android-extension'
复制代码


此时,我们可以直接用组件的 id 来操作 View 实例,例如:MainActivity .java


btn_login.setOnClickListener{
    // do something
}
复制代码


我们试试反编译这段代码,可以看到kotlin-android-extensions插件自动在 Activity 类中插入了以下代码:


MainActivity.class

public class MainActivity extends AppCompatActivity {
    private HashMap _$_findViewCache;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((Button) this._$_findViewCache(id.btn_login)).setOnClickListener((View.OnClickListener) null.INSTANCE);
    }
    public View _$_findCachedViewById(int var1) {
        if (this._$_findViewCache == null) {
            this._$_findViewCache = new HashMap();
        }
        View var2 = (View) this._$_findViewCache.get(Integer.valueOf(var1));
        if (var2 == null) {
            var2 = findViewById(var1);
            this._$_findViewCache.put(Integer.valueOf(var1), var2);
        }
        return var2;
    }
    public void _$_clearFindViewByIdCache() {
        if (this._$_findViewCache != null) {
            this._$_findViewCache.clear();
        }
    }
}
复制代码


可以看到,在访问R.id.*控件时,先在缓存集合_$_findViewCache中查找,有就直接返回,没有就通过 findViewById() 进行查找,并添加到缓存集合中。


另外还提供了一个_$_clearFindViewByIdCache()方法,用于在彻底替换界面视图时清除彻底缓存。在 Fragment#onDestroyView() 中,会调用该方法清除缓存,而 Activity 中没有。


4.3 简洁的 LeetCode 题解


在解算法题时,使用扩展函数可以让代码更简洁,表意性更强。举个例子,我们需要交换数组中的两个位置上的元素。相对于传统的写法,可以看到扩展函数的写法意思更清楚。


fun swap(arr: IntArray, from: Int, to: Int) {
    ...
}
swap(arr,0,1)
fun IntArray.swap(from: Int, toInt) {
    ...
}
arr.swap(0,1)
复制代码



5. 总结


  • 扩展可以在不修改类 / 不继承类的情况下,向一个类添加新函数或者新属性,更符合开闭原则。相对于传统 Java 的工具方法的调用方式更简单直接,表意性更强;
  • 扩展函数是定义在类外部的静态函数,函数的第一个参数是接收者类型,调用扩展时不会创建适配对象或者任何运行时的额外消耗。在 Java 中,我们只需要像调用普通静态方法那样调用扩展即可;
  • 标准库提供的函数中,run、with、apply 函数中的参数 block 是「T 的扩展函数」,所以采用 this 是扩展函数的接收者对象(receiver);also 和 let 参数 block 是「参数为 T 的函数」,所以采用 it 是唯一参数(argument)。
目录
相关文章
|
8天前
|
安全 Kotlin
Kotlin - 作用域函数
Kotlin - 作用域函数
|
5天前
|
Java Kotlin Python
Kotlin - 扩展成员
Kotlin - 扩展成员
13 2
Kotlin - 扩展成员
|
8天前
|
Java Kotlin Python
Kotlin - 扩展成员
Kotlin - 扩展成员
20 2
Kotlin - 扩展成员
|
12天前
|
Kotlin
Kotlin - 高阶函数与函数引用
Kotlin - 高阶函数与函数引用
25 3
Kotlin - 高阶函数与函数引用
|
20天前
|
Java Kotlin Python
​ Kotlin教程笔记(16) - 扩展成员
​ Kotlin教程笔记(16) - 扩展成员
​ Kotlin教程笔记(16) - 扩展成员
|
20天前
|
Kotlin
Kotlin教程笔记(21) -高阶函数与函数引用
Kotlin教程笔记(21) -高阶函数与函数引用
22 1
Kotlin教程笔记(21) -高阶函数与函数引用
|
6天前
|
IDE 开发工具 Kotlin
Kotlin - 函数与Lambda表达式
Kotlin - 函数与Lambda表达式
|
9天前
|
Kotlin
Kotlin教程笔记(21) -高阶函数与函数引用
Kotlin教程笔记(21) -高阶函数与函数引用
|
9天前
|
安全 Kotlin
Kotlin教程笔记(23) -作用域函数
Kotlin教程笔记(23) -作用域函数
|
12天前
|
安全 Kotlin
Kotlin - 作用域函数
Kotlin - 作用域函数
25 3