Kotlin 扩展函数和扩展属性的使用方法
Kotlin 能够扩展一个类的新功能而无需继承该类或者使用像装饰者这样的设计模式。 这通过叫做 扩展 的特殊声明完成。 例如,你可以为一个你不能修改的、来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。 这种机制称为 扩展函数 。此外,也有 扩展属性 , 允许你为一个已经存在的类添加新的属性。
前言
作为安卓开发,我们常常碰到这样的场景,需要把以dp为单位的值转化为以px为单位。这时候我们常会写一个Utils类,比如说
public class Utils { public static float dp2px(int dpValue) { return (0.5f + dpValue * Resources.getSystem().getDisplayMetrics().density); } }
在代码中直接调用 Utils.dp2px(100) 来使用,
val dp2px = Utils.dp2px(100)
如果用kotlin扩展函数的方式来实现,会是怎么调用呢?
val dp2px = 100.dp2px()
是不是很惊讶,100作为一个Int,竟然直接调用了一个dp2px方法,如果你去源码里找找,其实是没有个方法的。我们没有动源码,而是使用拓展函数的方式为Int增加了一个方法。
fun Int.dp2px(): Float { return (0.5f + this * Resources.getSystem().displayMetrics.density) }
扩展函数
我们再来举个🌰,有一个Person类如下
class Person(val name: String) { fun eat() { Log.i(name, "I'm going to eat") } fun sleep() { Log.i(name, "I'm going to sleep") } }
它有两个方法,一个是 eat 、一个是 sleep,调用的话就分别打印相应的Log。我们现在不想动Person类,但是又想给他增加一个新的方法,怎么做呢。我们可以新建一个文件 PersonExtensions.kt,再通过一下代码实现,就可以为 Person类新增一个 drink 方法啦。
fun Person.drink() { Log.i("Person", "${this.name}: I'm going to drink") }
声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。上面我们就是以 Person 作为一个扩展函数的接收类型,为其拓展来 drink 方法。我们在其方法中调用了 this ,这个 this 指的就是调用这个拓展方法的当前 Person 对象。
扩展函数调用的话也和普通的方法相同。但是你会发现IDE显示的方法颜色有点不一样。
由此也可以看出普通的方法和我们的拓展函数是不同的。下面我们来看看扩展函数的实际实现。
在 Android Studio 中,我们可以查看 kotlin 文件的字节码,然后再 Decompile 为 Java 代码。上面我们为 Person 扩展函数转为Java代码后如下。
@Metadata( mv = {1, 1, 15}, bv = {1, 0, 3}, k = 2, d1 = {"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0018\u0002\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"}, d2 = {"cook", "", "Lcom/chaochaowu/kotlinextension/Person;", "app_debug"} ) public final class PersonExtensionsKt { public static final void cook(@NotNull Person $this$cook) { Intrinsics.checkParameterIsNotNull($this$cook, "$this$cook"); Log.i("Person", $this$cook.getName() + ": I'm going to cook"); } }
妹想到啊,它原来是一个 static final 声明的静态方法,它的入参是一个 Person 类型,也就是我们之前的接收类型。那在Java代码中能不呢调用呢?
PersonExtensionsKt.cook(new Person("Bob"));
竟然也没有报错!由此可见,所谓扩展函数并不是真正的在类中增加了一个方法,而是通过外部文件的静态方法来实现,其实就是和Utils类一个道理。
因为将一个 Person 作为入参传入了方法中,所以我们也就可以在方法内对这个 Person 对象进行操作,这也就是在扩展方法中我们可以使用 this 来访问 Person 属性的原因。
再来看一个特殊的例子。
val s: String? = null s.isNullOrEmpty()
上面的代码中,s的值为null,我们用null去调用了一个方法,这会不会报错呢?按照以前的经验,一个null去调用一个方法,必然会报空指针的异常,但是上面的代码却是不会崩的。为什么哩?
其实 isNullOrEmpty 是 CharSequence? 的一个扩展方法,我们可以看一下它的源码。
@kotlin.internal.InlineOnly public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 }
contract这个契约方法这边我们不需要注意,不影响。主要是看 return this == null || this.length == 0 这句话。它先是判断了 this 是否为空,然后再判断this 的长度。根据我们上面讲的扩展函数的本质,我们可以很好的理解,为什么null可以调用这个方法的原因。因为上面的代码转为 Java 代码后是这样子的。
public static final boolean isNullOrEmpty(@Nullable CharSequence $this$isNullOrEmpty) { int $i$f$isNullOrEmpty = 0; return $this$isNullOrEmpty == null || $this$isNullOrEmpty.length() == 0; }
我们在用null调用这个扩展方法时,其实是将null作为一个参数传入这个方法中,先判断参数是否为null,再进行下一步判断,这当然不会崩溃。
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员, 仅仅是可以通过该类型的变量用点表达式去调用这个新函数,并将自身作为参数传入。
扩展属性
扩展属性和扩展函数类似,再举上面Person 的例子,我们对 Person 类稍作修改,为其增加 birthdayYear 字段,表示其出生的年份。
class Person(val name: String, val birthdayYear: Int) { fun eat() { Log.i(name, "I'm going to eat") } fun sleep() { Log.i(name, "I'm going to sleep") } }
我们现在要为 Person 增加年龄 age 的属性,但是不改 Person 类,怎么实现呢。和扩展函数一样,在其他文件中声明如下。
const val currentYear = 2019 val Person.age: Int get() = currentYear - this.birthdayYear
我们通过当前年份减去生日年份计算出 Person 的年龄。可以看到,age 是一个属性,而不是方法。这样我们就为 Person 增加了一个扩展属性。可以看看它转化为 Java 代码后的样子,和扩展函数没啥区别。
@Metadata( mv = {1, 1, 15}, bv = {1, 0, 3}, k = 2, d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\"\u000e\u0010\u0000\u001a\u00020\u0001X\u0086T¢\u0006\u0002\n\u0000\"\u0015\u0010\u0002\u001a\u00020\u0001*\u00020\u00038F¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\u0006"}, d2 = {"currentYear", "", "age", "Lcom/chaochaowu/kotlinextension/Person;", "getAge", "(Lcom/chaochaowu/kotlinextension/Person;)I", "app_debug"} ) public final class PersonExtensionsKt { public static final int currentYear = 2019; public static final int getAge(@NotNull Person $this$age) { Intrinsics.checkParameterIsNotNull($this$age, "$this$age"); return 2019 - $this$age.getBirthdayYear(); } }
上面我们声明的是一个 val,当然也可以声明一个 var,不过 var 的话需要同时定义其 get 和 set 方法。
由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。他们的行为只能由显式提供的 getters/setters 定义。
总结
在 Java 中,我们要扩展一个类时,常常是继承该类或者用装饰者模式类似的设计模式来实现,Kotlin 扩展函数和扩展属性为这种需求提供了一种新思路,并且也可以作为 Utils 类的另外一种选择,值得一试。