《Kotlin核心编程》笔记:特设多态、运算符重载和扩展函数

发布时间:2023年12月20日

多态的不同形式

Kotlin 的扩展函数其实只是多态的表现形式之一。

子类型多态

继承父类后,用子类实例使用父类的方法,例如:

然后我们就可以使用父类DatabaseHelper的所有方法。这种用子类型替换超类型实例的行为,就是我们通常说的子类型多态。

class CustomerDatabaseHelper(context: Context) : SQLiteOpenHelper(context) {

    override fun onUpgrade(p0: SQLiteDatabase?, p1: Int, p2: Int) {}

    override fun onCreate(db: SQLiteDatabase) {
        val sql = "CREATE TABLE if not exists $tableName ( id integer PRIMARY KEY);"
        db.execSQL(sql)
    }
}

然后我们就可以使用父类DatabaseHelper的所有方法。这种用子类型替换超类型实例的行为,就是我们通常说的子类型多态。

参数多态

在完成数据库的创建之后,现在我们要把客户(Customer)存入客户端数据库中。可能会写这样一个方法:

fun persist(customer: Customer) {
	db.save(customer.uniqueKey, customer)
}

随着需求的变动,我们可能还会持久化多种类型的数据。如果每种类型都写一个presist方法,多少有些烦琐,通常我们会抽象一个方法来处理不同类型的持久化。因为我们采用键值对的方式存储,所以需要获取不同类型对应的uniqueKey

interface IKey {
    val uniqueKey: String
}

class ClassA(override val uniqueKey: String) : IKey 
class ClassB(override val uniqueKey: String) : IKey 

这样,class A、B 都已经具备uniqueKey。我们可以将persist进行如下改写:

fun <T : IKey> persist(t: T) {
    db.save(t.uniqueKey, t)
}

以上的多态形式我们可以称之为参数多态,其实最常见的参数多态的形式就是泛型

对第三方类进行扩展

假使当对应的业务类ClassAClassB是第三方引入的,且不可被修改时,如果我们要想给它们扩展一些方法,比如将对象转化为Json,利用之前介绍的多态技术就会显得比较麻烦。

利用Kotlin支持的扩展语法,就能给 ClassAClassB添加方法或属性,从而换一种思路来解决上面的问题。

fun ClassA.toJson(): String = { 
	......
}

需要注意的是,扩展属性和方法的实现运行在ClassA实例,它们的定义操作并不会修改ClassA类本身。这样就为我们带来了一个很大的好处,即被扩展的第三方类免于被污染,从而避免了一些因父类修改而可能导致子类出错的问题发生。

当然,在 Java 中我们可以依靠其他的办法比如设计模式来解决,但相较而言依靠扩展的方案显得更加方便且合理,这其实也是另一种被称为特设多态的技术。

特设多态与运算符重载

可能你对特设多态这个概念并不是很了解,我们来举一个具体的例子。当你想定义一个通用的sum方法时,也许会在Kotlin中这么写:

fun <T> sum(x : T, y : T) : T = x + y 

但编译器会报错,因为某些类型T的实例不一定支持加法操作,而且如果针对一些自定义类,我们更希望能够实现各自定制化的“加法语义上的操作”。

如果把参数多态做的事情打个比方:它提供了一个工具,只要一个东西能“切”,就用这个工具来切割它。然而,现实中不是所有的东西都能被切,而且材料也不一定相同。更加合理的方案是,你可以根据不同的原材料来选择不同的工具来切它。

再换种思路,我们可以定义一个通用的Summable接口,然后让需要支持加法操作的类来实现它的plusThat方法。就像这样:

interface Sumable<T> {
    fun plusThat(that: T): T
}

data class Len(val v: Int) : Sumable<Len> {
    override fun plusThat(that: Len) = Len(this.v + that.v)
}

可以发现,当我们在自定义一个支持plusThat方法的数据结构如Len时,这种做法并没有什么问题。然而,如果我们要针对不可修改的第三方类扩展加法操作时,这种通过子类型多态的技术手段也会遇到问题。

于是,你又想到了 Kotlin 的扩展,也就是叫作“ 特设多态” 技术。特设多态可以理解为:一个多态函数是有多个不同的实现,依赖于其实参而调用相应版本的函数。

针对以上的例子,我们完全可以采用扩展的语法来解决问题。此外,Kotlin原生支持了一种语言特性来很好地解决问题,这就是运算符重载。借助这种语法,我们可以完美地实现需求。代码如下:

data class Area(val value: Double)

operator fun Area.plus(that: Area): Area {
    return Area(this.value + that.value)
}

fun main() {
    println(Area(1.0) + Area(2.0)) // 运行结果: Area(value=3.0)
}

operator关键字的作用是:将一个函数标记为重载一个操作符或者实现一个约定。

注意,这里的 plus 是 Kotlin 规定的函数名。除了重载加法,我们还可以通过重载减法(minus)、乘法(times)、除法(div)、取余(mod)(Kotlin 1.1 版本开始被rem替代)等函数来实现重载运算符。

此外,kotlin的一些基础语法也是利用运算符重载来实现的,如:

扩展:为别的类添加方法、属性

对开发者而言,在修改现有代码的时候,应当遵守设计模式中 OO 设计原则中的开闭原则,然而实际情况并不乐观,比如在进行 Android 开发的时候,为了实现某个需求,我们引入了一个第三方库。但某一天需求发生了变动,当前库无法满足,且库的作者暂时没有升级的计划。这时候也许你就会开始尝试对库源码进行修改。这就违背了开放封闭原则。随着需求的不断变更,问题可能就会如滚雪球般增长。

Java中一种惯用的应对方案是让一个子类继承第三方库的类,然后在其中添加新功能。然而,强行的继承可能违背“ 里氏替换原则” 。

Kotlin的扩展语言特性为我们提供了一种更合理的方案,通过扩展一个类的新功能而无须继承该类,在大多数情况下都是一种更好的选择,从而我们可以合理地遵循OO设计原则。

扩展函数的接收者类型 (recievier type)

MutableList<Int>为例,我们为其扩展?个exchange方法,代码如下:

fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) { 
	val tmp = this[fromlndex]
	this[fromlndex] = this[tolndex] 
	this[tolndex] = tmp
}

MutableList<T>是 Kotlin 标准库Collections中的List容器类,这里作为recieviertypeexchange是扩展函数名。其余和 Kotlin 声明一个普通函数并无区别。

Kotlin 的this要比Java更灵活,这里扩展函数体里的this代表的是接收者类型的对象。这里需要注意的是:Kotlin 严格区分了接收者是否可空如果你的函数是可空的,你需要重写一个可空类型的扩展函数

我们可以非常方便地对该函数进行调用,代码如下:

val list = mutableListOf(1,2,3) 
list.exchange(1,2)

扩展函数的实现机制

扩展函数的使用如此方便,会不会对性能造成影响呢?我们以前面的MutableList<Int>.exchange为例,它对应的 Java 代码如下:

public final class ExSampleKt {

    public static final void exchange(@NotNull List Sreceiver, int fromlndex, int tolndex) { 
    	Intrinsics.checkParameterlsNotNull($receiver, "$receiver");
        int tmp = ((Number)$receiver.get(fromlndex)).intValue();
        Sreceiver.set(fromlndex, $receiver.get(tolndex));
        Sreceiver.set(tolndex, Integer.valueOf(tmp));
    }
}

可以看出,扩展函数被定义为了一个静态方法,而 Java 的静态方法的特点就是:独立于该类的任何实例对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。

因此,我们可以得出结论:扩展函数不会带来额外的性能消耗

扩展函数的作用域

一般来说,我们习惯将扩展函数直接定义在包内,例如前面的exchange例子,我们可以将其放在com.example.extension包下:

package com.example.extension

fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) { 
	val tmp = this[fromlndex]
	this[fromlndex] = this[tolndex] 
	this[tolndex] = tmp
}

我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用 Java 全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理。

class Extends {
	fun MutableList<Int>.exchange(fromindex : Int, tolndex : Int) { 
		val tmp = this[fromlndex]
		this[fromlndex] = this[tolndex] 
		this[tolndex] = tmp
	}
}

但是当扩展函数定义在Extends类内部时,你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。

你可能会猜想,是不是它被声明为private方法了?但即便你尝试在exchange方法前加上public关键字也依旧无法调用到(实际上 Kotlin 中成员方法默认就是用public修饰的)。

是什么原因呢?借助 IDEA 我们可以查看到它对应的 Java 代码,这里展示关键部分:

public static final class Extends {

    public final void exchange(@NotNull List Sreceiver, int fromlndex, int tolndex) {
        Intrinsics.checkParameterlsNotNull(Sreceiver, "$receiver");
        int tmp = ((Number)Sreceiver.get(fromlndex)).intValue();
        Sreceiver.set(fromlndex, $receiver.get(tolndex));
        Sreceiver.set(tolndex, Integer.valueOf(tmp));
    }
}

我们看到,exchange方法上已经没有static关键字的修饰了。所以当扩展方法在一个Class内部时,我们只能在该类和该类的子类中进行调用

或者,另一个解决方法是我们可以借助 Kotlin 的 with() 函数来解决:

object Extends {
    fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
        val tmp = this[fromIndex]
        this[fromIndex] = this[toIndex]
        this[toIndex] = tmp
    }
}

fun main() {
    val list = mutableListOf(1,2,3)
    with(Extends) {
        list.exchange(1,2)
    }
}

这里 Extends 定义为 object 单例,with(Extends) 函数的 {} 内部自动变成 Extends 实例的作用域范围,因此可以在其中正常的访问扩展函数了。如果 Extends 是一个已有的类,不方便改成 object 类,那么可以选择把扩展函数的定义包在一个伴生对象中:

class Extends {
    companion object {
        fun MutableList<Int>.exchange(fromIndex:Int, toIndex:Int) {
            val tmp = this[fromIndex]
            this[fromIndex] = this[toIndex]
            this[toIndex] = tmp
        }
    }
}

fun main() {
    val list = mutableListOf(1,2,3)
    with(Extends) {
        list.exchange(1,2)
    }
}

这样同样能达到效果。

扩展属性

与扩展函数类似,我们还能为一个类添加扩展属性。比如我们想给 MutableList<Int> 添加一个判断和是否为偶数的属性sumIsEven

val MutableList<Int>.sumlsEven: Boolean 
	get() = this.sum() % 2 == 0

就可以像扩展函数一样调用它:

val list = mutableListOf(2,2,4) 
list.sumlsEven

但是扩展属性不支持默认值,如下写法会报错:

// 编译错误:扩展属性不能有初始化器
val MutableList<Int>.sumlsEven: Boolean = false 
	get() = this.sum() % 2 == 0

这是为什么呢?

其实,与扩展函数一样,其本质也是对应 Java 中的静态方法(我们反编译成 Java 代码后会看到一个getSumIsEven的静态方法)。由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的。

为伴生对象定义扩展函数

在Kotlin中,如果你需要声明一个静态的扩展函数,开发者必须将其定义在伴生对象(companion object)上。所以我们需要这样定义带有伴生对象的类:

class Son {
	companion object {
		val age = 10
	}
}

现在Son类中已经有一个伴生对象,如果我们现在不想在Son中定义扩展函数,而是在Son的伴生对象上定义,可以这么写:

fun Son.Companion.foo() {
	println("age = Sage")
}

这样,我们就能在Son没有实例对象的情况下,也能调用到这个扩展函数,语法类似于Java的静态方法:

fun main() { 
	Son.foo()
}

一切看起来都很顺利,但是当我们想让第三方类库也支持这样的写法时,我们发现,并不是所有的第三方类库中的类都存在伴生对象,我们只能通过它的实例来进行调用,但这样会造成很多不必要的麻烦。

成员方法优先级总高于扩展函数

已知有如下类:

class Son {
	fun foo() = println("son called member foo") 
}

假如我们不小心为Son写了一个同名的扩展函数:

fun Son.foo() = println("son called extention foo") 

在调用时,我们希望调用的是扩展函数foo(),但是输出结果是成员函数的,不会符合我们的预期。

这表明:当同时存在同名的扩展函数和现有类的成员方法时,Kotlin将会默认使用类的成员方法覆盖同名扩展方法。

看起来似乎不够合理,并且很容易引发?些问题:我定义了新的方法,为什么还是调用到了旧的方法?

但是换一个角度思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了。所以在使用时,我们必须注意:同名的类成员方法的优先级总是高于扩展函数

类的实例与接收者的实例

当在扩展函数里调用 this 时,指代的是接收者类型的实例。那么如果这个扩展函数声明在一个object内部,我们如何通过this获取到该object的实例呢?参考如下代码:

class Son {
    fun foo() {
        println("foo in Class Son")
    }
}
object Parent {
    fun foo() {
        println("foo in Class Parent")
    }
    fun Son.foo2() {
        this.foo()
        this@Parent.foo()
    }
}
fun main() { 
    val son = Son()
    with(Parent) {
        son.foo2() 
    } 
}

这里我们可以用this@类名来强行指定调用的this

标准库中的扩展函数:run、let、also、takeIf

Kotlin 标准库中有?些非常实用的扩展函数,除了之前我们接触过的applywith函数之外,我们再来了解下letrunalsotakeIf

先来看下run方法,它是利用扩展实现的,定义如下:

public inline fun <T, R> T.run(block: T.() -> R): R { 
    return block()
}

简单来说,run是任何类型T的通用扩展函数,run中执行了返回类型为R的扩展函数block,最终返回该扩展函数的结果。

run函数中我们拥有一个单独的作用域,能够在其中定义一个新的变量,并且它的作用域只存在于run函数中。

fun testFoo() {
    val nickName = "Prefert"
    run {
        val nickName = "YarenTang"
        println(nickName) // YarenTang
    }
    println(nickName) // Prefert
}

这个范围函数本身似乎不是很有用,但是相比范围,还有一点不错的是,它返回范围内最后一个对象。

例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果没有登录则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑:

run {
    if (!islogin) loginDialog else getNewAccountDialog 
}.show()

letletapply类似,唯一不同的是返回值:apply返回的是原来的对象,而let返回的是闭包里面的值。

public inline fun <T, R> T.let(block: (T) -> R): R { 
    return block(this)
}
data class Student(val age: Int)

class Kot {
    val student: Student? = getStu() 
    fun dealStu() {
        val result = student?.let {
            println(it.age)
            it.age
        } 
    }
}

由于let函数返回的是闭包的最后一行,当student不为null的时候,才会打印并返回它的年龄。与run一样,它同样限制了变量的作用域。

also:它像是letapply的加强版

public inline fun <T> T.also(block: (T) -> Unit): T { 
    block(this)
    return this
}

apply一样,它返回的是该函数的接收者

class Kot {
    val student: Student? = getStu()
    var age = 0

    fun dealStu() {
        val result = student?.also { stu ->
            this.age += stu.age
            println(this.age)
            println(stu.age) 
        } 
    }
}

我将它的隐式参数指定为stu,假设student?不为空,我们会发现返回了student,并且总年龄age增加了。

值得注意的是:这里如果使用apply,由于它执行的blockT类型的扩展函数,this将指向stu而不是Kot,此处我们将无法调用到Kot下的age

takeIf:如果我们不仅仅只想判空,还想加入条件,这时let可能显得有点不足。让我们来看看takeIf

public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? { 
    return if (predicate(this)) this else null
}

如果我们想判断成年的学生再执行操作,可以这样写:

val result = student.takelf {it.age >= 18}.let {...} 

我们发现,这与集合中的filter异曲同工,不过takeIf只操作单条数据。与takeIf相反的还有takeUnless,即接收器不满足特定条件才会执行。

除了以上这些,Kotlin中还有其他很多方便的扩展函数。

Android 中的扩展应用

优化 Snackbar

几年前,它被添加到Android支持库中,以取代Toast。它解决了一些问题并引入了一种全新的外观,基本使用方式如下:

Snackbar.make(parentView, message_text, duration) 
	.setAction(action_text, click_listener)
	.show(); 

但是实际中使用它的API会给代码增加不必要的复杂性:我们不希望每次都定义我们想要显示消息的时间,并且在填充一堆参数后,为什么我们还要额外调用 show()

著名的开源项目Anko拥有Snackbar的辅助函数,使其更易于使用并使代码更简洁:

snackbar(parentView, action_text, message_text) { click_listener } 

其中一些参数是可选的,所以我们一般这么使用:

snackbar(parentView, "message") 

它的部分源码如下:

inline fun View.snackbar(message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) {
    Snackbar.make(this, message, Snackbar.LENGTH_SHORT)
        .setAction(actionText, action)
        .apply { show() }
}

如果想让它更短:

snackbar("message") 

可以自己定义扩展函数:

inline fun Activity.snackbar(message: String) = snackbar(find(R.id.content), message)
inline fun Fragment.snackbar(message: String) = snackbar(activity.find(R.id.content), message)

View并不一定附加在Activity上,我们要做出防御式判断,即:在我们尝试显示Snackbar之前,我们必须确保Viewcontext属性隐藏了一个Activity实例:

inline fun View.snackbar(message: String) {
    val activity = context 
    if (activity is Activity) {
        snackbar(activity.find(android.R.id.content), message)
    } else {
        throw IllegalStateException("视图必须要承载在Activity上.")
    }
}

用扩展函数封装 Utils

比如,我们现在有一个判断手机网络是否可用的方法:

Boolean isConnected = NetworkUtils.isMobileConnected(context); 

作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。

我们期望的是下面这样的使用方式:

Boolean isConnected= context.isMobileConnected(); 

由于Context是Andorid SDK自带的类,我们无法对其进行修改,在Kotlin中,我们通过扩展函数就能简单地实现:

值得?提的是,在Android中对Context的生命周期需要进行很好地把控。这里我们应该使用ApplicationContext,防止出现生命周期不一致导致的内存泄漏或者其他问题。

除了上述方法,我们还有许多这样通用的代码,我们可以将它们放入不同的文件下。包括上面提到的Snackbar,我们也可以为其创建一个SnackbarUtils,这样会提供非常多的便利。但是需要注意的是,我们不能滥用这个特性。

解决烦人的 findViewById

对于Android开发者来说,对findViewById()这个方法?定不会陌生:在我们对视图控件操作前,我们需要通过findViewById方法来找到其对应的实例。

因为?个界面里的控件的数量可能会非常多,所以在 Android 开发早期我们通常都会看到一大片的findViewById(R.id.view_id)样板代码。而在老版本SDK中,在findBiewById获取到View之后,我们甚至还需要进行强制类型转换。

在Kotlin中我们可以利用扩展函数来简化:

fun <T : View> Activity._view(@ldRes id: Int): T { 
	return findViewByld(id) as T
}

调用:

loginButton = _view(R.id.btn_login); 
nameEditText = _view(R.id.et_name); 

现在调用起来是比较方便了,但是部分极简主义的读者可能会想:当前我们还是需要创建loginButtonnameEditText的实例,但是这些实例似乎只充当了?个“临时变量”的角色,我们依靠它进行一些点击事件绑定(onlick)、赋值操作后好像就没什么用处了。能不能将其也省略掉,直接对R.id.*操作呢?答案是可以,在Kotlin中我们可以利用高阶函数,做如下改动(此处以简化onclick为例子):

fun Int.onClick(click: () -> Unit) {
    // _view 为我们之前定义的简化版 findViewByld
    val tmp = _view<View>(this).apply {
        setOnClickListener { 
            click()
        }
    }
}

我们就可以这样绑定登录按钮的点击事件:

R.id.btn_login.onClick { println("Login...") } 

可能有强迫症的读者会受不了R.id.xx这样的写法,并且每次都要写R.id前缀,某种情况下也会造成烦琐。

那还有更简洁的写法吗?答案是肯定的,Kotlin为我们提供了一个扩展插件:

apply plugin: 'kotlin-android-extensions'  
btn_login.setOnClickListener {
	println("MainKotlinActivity onClick Button") 
}

虽然是省略了R.id.几个字符,但是引入是否会造成性能问题? 让我们先对其反编译,看看其对应Java代码中是如何实现的:

public final class MainActivity extends BaseActivity {
    private HashMap<Integer, View> _$_findViewCache;

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131296283);
        ((TextView) this._$_findCachedViewById(id.label)).setText((CharSequence));
        ((TextView) this._$_findCachedViewById(id.label)).setOnClickListener((OnClickListener));
        ((Button) this._$_findCachedViewById(id.btn)).setOnClickListener((OnClickListener));
    }

    public View _$_findCachedViewById(int var1) {
        if (this._$_findViewCache == null) {
            this._$_findViewCache = new HashMap();
        }

        View var2 = (View) this._$_findViewCache.get(var1);
        if (var2 == null) {
            var2 = this.findViewById(var1);
            this._$_findViewCache.put(var1, var2);
        }

        return var2;
    }

    public void _$_clearFindViewByIdCache() {
        if (this._$_findViewCache != null) {
            this._$_findViewCache.clear();
        }
    }
}

你会惊喜地发现,在第一次使用控件的时候,在缓存集合中进行查找,有就直接使用,没有就通过findViewById进行查找,并添加到缓存集合中。其还提供了$clearFindViewByIdCache()方法用于清除缓存,在我们想要彻底替换界面控件时可以使用。

注意:FragmentonDestroyView()方法中默认调用了$clearFindViewByIdCache()清除缓存,而Activity没有。

虽然 KAE( Kotlin Android Extensions)很方便,但是很遗憾,由于某些原因,KAEKotlin 1.4.2 开始被官方宣布废弃,而在 Kotlin1.7 版本中直接移除了。。(具体可以看这里

但是如果你喜欢,仍然可以自己模仿它的源码来实现一套自己维护。

或者,我们可以使用其他替代方案,比如viewbinding,在gradle添加如下配置:

android {
     viewBinding {
        enabled = true
     }
}

然后使用效果同KAE一样,不过这和kotlin特性没多大关系了。( 系统会为该模块中的每个XML布局文件生成一个绑定类)

扩展不是万能的

静态与动态调度

已知我们有以下Java类:

class Base {
    public void foo() {
        System.out.println(("I'm Base foo!"));
    }
}
class Extended extends Base {
    @Override
    public void foo() {
        System.out.println(("I'm Extended foo!"));
    }
}

Base base = new Extended();
base.foo();

我们声明一个名为base的变量,它具有编译时类型Base和运行时类型Extended。当我们调用时,base.foo()将动态调度该方法,这意味着运行时类型(Extended)的方法被调用。

当我们调用重载方法时,调度变为静态并且仅取决于编译时类型。

void foo(Base base) {
	...
}
void foo(Extended extended) {
	...
}
public static void main(String] args) {
	Base base = new Extended();
	foo(base);
}

在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。

扩展函数始终静态调度

可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它,但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。

为了便于理解,我们举一个例子:

open class Base
class Extended : Base()

fun Base.foo() = "I'm Base.foo!"
fun Extended.foo() = "I'm Extended.foo!"

fun main() {
    val instance: Base = Extended()
    val instance2 = Extended()
    println(instance.foo()) // Output: I'm Base.foo!
    println(instance2.foo()) // Output: I'm Extended.foo!
}

正如我们所说,由于只考虑了编译时类型,第1个打印将调用Base.foo(),而第2个打印将调用Extended.foo()

类中的扩展函数

如果我们在类的内部声明扩展函数,那么它将不是静态的。如果该扩展函数加上open关键字,我们可以在子类中进行重写(override)。这是否意味着它将被动态调度?这是一个比较尴尬的问题:当在类内部声明扩展函数时,它同时具有调度接收器和扩展接收器

调度接收器和扩展接收器的概念

  • 扩展接收器(extension receiver):与 Kotlin 扩展密切相关的接收器,表示我们为其定义扩展的对象。
  • 调度接收器(dispatch receiver):扩展被声明为成员时存在的一种特殊接收器,它表示声明扩展名的类的实例。
class X {
	fun Y.foo() = " I'm Y.foo" 
}

在上面的例子中,X是调度接收器而Y是扩展接收器。如果将扩展函数声明为open,则它的调度接收器只能是动态的,而扩展接收器总是在编译时解析。

这样说你可能还不是很明白,我们还是举一个例子帮助理解:

open class Base
class Extended : Base()

open class X {
    open fun Base.foo() {
        println("I'm Base.foo in X")
    }
    open fun Extended.foo() {
        println("I'm Extended.foo in X")
    }
    fun deal(base: Base) {
        base.foo()
    }
}
class Y : X() {
    override fun Base.foo() {
        println("I'm Base.foo in Y")
    }
    override fun Extended.foo() {
        println("I'm Extended.foo in Y")
    }
}

fun main() {
    X().deal(Base()) // 输出: I'm Base.foo in X
    Y().deal(Base()) // 输出: I'm Base.foo in Y 即 dispatch receiver 被动态调度

    X().deal(Extended()) // 输出: I'm Base.foo in X 即 extension receiver 被静态调度
    Y().deal(Extended()) // 输出: I'm Base.foo in Y
}

聪明的你可能会注意到,Extended扩展函数始终没有被调用,并且此行为与我们之前在静态调度例子中所看到的一致。决定两个Base类扩展函数执行哪一个,直接因素是执行deal方法的类的运行时类型。

通过以上例子,我们可以总结出扩展函数几个需要注意的地方:

  • 如果该扩展函数是顶级函数或成员函数,则不能被覆盖;
  • 我们无法访问其接收器的非公共属性;
  • 扩展接收器总是被静态调度。

被滥用的扩展函数

fun Context.loadImage(url: String, imageView: ImageView) {
    GlideApp.with(this)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(imageView) 
}

// ImageActivity.kt 中使用
...
this.loadlmage(url, imgView) 
...

也许你在用的时候并没有感觉出什么奇怪的地方,但是实际上,我们并没有以任何方式扩展现有类。上述代码仅仅为了在函数调用的时候省去参数,这是一种滥用扩展机制的行为。

我们知道,Context作为“God Object”,已经承担了很多责任。

我们基于Context扩展,还很可能产生ImageView与传入上下文周期不一致导致的很多问题。

正确的做法应该是在ImageView上进行扩展:

fun ImageView.loadImage(url: String) {
    GlideApp.with(this.context)
        .load(url)
        .placeholder(R.mipmap.img_default)
        .error(R.mipmap.ic_error)
        .into(this)
}

// Example usage
imageView.loadImage("https://example.com/image.jpg")

这样在调用的时候,不仅省去了更多的参数,而且ImageView的生命周期也得到了保证。

实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片请求框架进行二次封装:

object ImageLoader {
    fun with(context: Context, url: String, imageView: ImageView) {
        GlideApp.with(context)
            .load(url)
            .placeholder(R.mipmap.img_default)
            .error(R.mipmap.ic_error)
            .into(imageView)
    }
}

// Example usage
ImageLoader.with(context, "https://example.com/image.jpg", imageView)

所以,虽然扩展函数能够提供许多便利,我们还是应该注意在恰当的地方使用它,否则会造成不必要的麻烦。

文章来源:https://blog.csdn.net/lyabc123456/article/details/135034921
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。