多态的不同形式
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)
}
以上的多态形式我们可以称之为参数多态,其实最常见的参数多态的形式就是泛型。
对第三方类进行扩展
假使当对应的业务类ClassA
、ClassB
是第三方引入的,且不可被修改时,如果我们要想给它们扩展一些方法,比如将对象转化为Json
,利用之前介绍的多态技术就会显得比较麻烦。
利用Kotlin支持的扩展语法,就能给 ClassA
、ClassB
添加方法或属性,从而换一种思路来解决上面的问题。
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
容器类,这里作为recieviertype
,exchange
是扩展函数名。其余和 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 标准库中有⼀些非常实用的扩展函数,除了之前我们接触过的apply
、with
函数之外,我们再来了解下let
、run
、also
、takeIf
。
先来看下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()
let
:let
和apply
类似,唯一不同的是返回值: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
:它像是let
和apply
的加强版
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
,由于它执行的block
是T
类型的扩展函数,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
之前,我们必须确保View
的context
属性隐藏了一个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);
现在调用起来是比较方便了,但是部分极简主义的读者可能会想:当前我们还是需要创建loginButton
、nameEditText
的实例,但是这些实例似乎只充当了⼀个“临时变量”的角色,我们依靠它进行一些点击事件绑定(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()
方法用于清除缓存,在我们想要彻底替换界面控件时可以使用。
注意:Fragment
的onDestroyView()
方法中默认调用了$clearFindViewByIdCache()
清除缓存,而Activity
没有。
虽然 KAE( Kotlin Android Extensions)很方便,但是很遗憾,由于某些原因,KAE 从 Kotlin 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)
所以,虽然扩展函数能够提供许多便利,我们还是应该注意在恰当的地方使用它,否则会造成不必要的麻烦。