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

多态的不同形式

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.extensionfun 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 = 0fun dealStu() {val result = student?.also { stu ->this.age += stu.ageprintln(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 为我们之前定义的简化版 findViewByldval 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 {@Overridepublic 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 XY().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)

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/227311.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】双链表的定义和操作

目录 1.双链表的定义 2.双链表的创建和初始化 3.双链表的插入节点操作 4.双链表的删除节点操作 5.双链表的查找节点操作 6.双链表的更新节点操作 7.完整代码 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助…

WPF-UI HandyControl 控件简单实战

文章目录 前言UserControl简单使用新建项目直接新建项目初始化UserControlGeometry:矢量图形额外Icon导入最优解决方案 按钮Button切换按钮ToggleButton默认按钮图片可切换按钮加载按钮切换按钮 单选按钮和复选按钮没有太大特点&#xff0c;就不展开写了总结 DataGrid数据表格G…

网络安全-等保测评相关知识

什么是等保测评&#xff1f; 信息安全等级保护&#xff0c;是对信息和信息载体按照重要性等级分级别进行保护的一种工作&#xff0c;其目的就是对信息系统安全防护体系能力的分析与确认&#xff0c;发现存在的安全隐患&#xff0c;帮助运营使用单位认识不足, 及时改进&#xff…

详细了解stm32---按键

提示&#xff1a;永远支持知识文档免费开源&#xff0c;喜欢的朋友们&#xff0c;点个关注吧&#xff01;蟹蟹&#xff01; 目录 一、了解按键 二、stm32f103按键分析 三、按键应用 一、了解按键 同学们&#xff0c;又见面了o(*&#xffe3;▽&#xffe3;*)ブ&#xff0c;最…

vue制作简易日历

你可以使用Vue.js来制作一个简易日历。&#xff1a; <!DOCTYPE html> <html> <head><title>Vue简易日历</title><script src"https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script><style>table {border-colla…

C++ Qt开发:Tab与Tree组件实现分页菜单

Qt 是一个跨平台C图形界面开发库&#xff0c;利用Qt可以快速开发跨平台窗体应用程序&#xff0c;在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置&#xff0c;实现图形化开发极大的方便了开发效率&#xff0c;本章将重点介绍tabWidget选择夹组件与TreeWidget树形选择组件…

43 贪心算法 -第K个排列

题 目 描 述 n 个 字 共 有 n &#xff01; 种 排 列 。 给 定 参 数 n &#xff0c; 从 1 到 n 会 有 n 个 整 数 &#xff1a; 123 按 大 小 顺 升 序 列 出 所 有 排 列 的 情 况 &#xff0c; 并 己 当 n 3 时 &#xff0c; 所 有 排 列 如 下 &#xff1a; “ 123 …

升华 RabbitMQ:解锁一致性哈希交换机的奥秘【RabbitMQ 十】

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 升华 RabbitMQ&#xff1a;解锁一致性哈希交换机的奥秘【RabbitMQ 十】 前言第一&#xff1a;该插件需求为什么需要一种更智能的消息路由方式&#xff1f;一致性哈希的基本概念&#xff1a; 第二&…

【Linux】MySQL 数据库安装配置教程(Ubuntu 22.04)

前言 MySQL是一个流行的开源关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;广泛用于Web应用程序的后端数据存储&#xff0c;如许多动态网站、电子商务系统和在线出版物等。 MySQL具有高性能、可靠性和易用性的特点&#xff0c;它支持大型数据库&#xff0c;…

Web应用安全—信息泄露

从书本和网上了解到Web应用安全的信息泄露的知识&#xff0c;今天跟大家分享点。 robots.txt泄漏敏感信息 漏洞描述&#xff1a;搜索引擎可以通过robots文件可以获知哪些页面可以爬取&#xff0c;哪些页面不可以爬取。Robots协议是网站国际互联网界通行的道德规范&#xff0c;其…

UniGUI之提示信息MessageDlg及获得信息Prompt

UniGui的信息弹出框MessageDlg的原型定义如下&#xff1a; procedure MessageDlg(const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; CallBack: TUniDialogCallBackAnonProc); 1. 其中DlgType(对话框架的类型) 1、mtConfirmation 2、mtCustom 3、mtError…

【Java】使用递归的方法获取层级关系数据demo

使用递归来完善各种业务数据的层级关系的获取 引言&#xff1a;在Java开发中&#xff0c;我们通常会遇到层层递进的关系型数据的获取问题&#xff0c;有时是树状解构&#xff0c;或金字塔结构&#xff0c;怎么描述都行&#xff0c;错综复杂的关系在程序中还是可以理清的。 这…

uniGUI之上传文件UniFileUploadButton

TUniFileUploadButton主要属性&#xff1a; Filter: 文件类型过滤&#xff0c;有图片image/* audio/* video/*三种过滤 MaxAllowedSize: 设置文件最大上传尺寸&#xff1b; Message&#xff1a;标题以及消息文本&#xff0c;可翻译成中文 TUniFileUploadButton控件 支持多…

K8S(八)—有、无状态.md

目录 无状态有状态&#xff08;StatefulSet&#xff09;介绍应用应用21. **状态持久性&#xff1a;**2. **水平扩展&#xff1a;**3. **存储和卷&#xff1a;**4. **Pod标识和网络标识&#xff1a;**5. **部署和更新&#xff1a;** 有状态和无状态对比1. 概念&#xff1a;2. 数…

【OpenCV】实时屏幕捕获

文章目录 前言基本思路安装依赖包实时捕获屏幕画面转换屏幕画面数据调用窗体显示屏幕截取画面增加实时捕获时间保存实时视频流效果图完整实现代码 前言 日常中如果需要进行大数据分析&#xff0c;那么就要记录用户的使用情况和数据分析。 实时屏幕捕获就可以很好地获取数据&a…

云原生之深入解析Linkerd Service Mesh的功能和使用

一、简介 Linkerd 是 Kubernetes 的一个完全开源的服务网格实现&#xff0c;它通过为你提供运行时调试、可观测性、可靠性和安全性&#xff0c;使运行服务更轻松、更安全&#xff0c;所有这些都不需要对代码进行任何更改。Linkerd 通过在每个服务实例旁边安装一组超轻、透明的…

Other -- 了解网上服务器(ECS、VPS)

喜欢在公网上自己做网站发布的就会研究网上的售卖的各种服务器&#xff0c;下面简单了解下&#xff1a; 1. 虚拟专用服务器&#xff08;VPS,VirtualPrivateServer&#xff09; 借助容器或者虚拟化技术&#xff0c;将一台物理服务器分割成多个虚拟服务器&#xff0c;每个服务器…

MX6ULL学习笔记(十二)Linux 自带的 LED 灯

前言 前面我们都是自己编写 LED 灯驱动&#xff0c;其实像 LED 灯这样非常基础的设备驱动&#xff0c;Linux 内 核已经集成了。Linux 内核的 LED 灯驱动采用 platform 框架&#xff0c;因此我们只需要按照要求在设备 树文件中添加相应的 LED 节点即可&#xff0c;本章我们就来学…

Python基础05-函数

零、文章目录 Python基础05-函数 1、函数的作用及其使用步骤 &#xff08;1&#xff09;函数的作用 在Python实际开发中&#xff0c;我们使用函数的目的只有一个“让我们的代码可以被重复使用” 函数的作用有两个&#xff1a; ① 代码重用&#xff08;代码重复使用&#xf…

【AI工具】GitHub Copilot IDEA安装与使用

GitHub Copilot是一款AI编程助手&#xff0c;它可以帮助开发者编写代码&#xff0c;提供代码建议和自动完成功能。以下是GitHub Copilot在IDEA中的安装和使用步骤&#xff1a; 安装步骤&#xff1a; 打开IDEA&#xff0c;点击File -> Settings -> Plugins。在搜索框中输…