背景
在kotlin中经常看到这样的if语句:
//s是一个String类型的对象
if(s.isNullOrEmpty()){
//执行空对象或空字符串的逻辑
}
笔者不禁想问,如果s是空对象,调用它的函数不会抛出空指针吗?
它是如何检查被调用的对象s是空对象的呢?
查阅资料后,发现原来这是kotlin的扩展函数的特性:
被扩展的类后面添加?.
再接扩展函数名即表示可以在空对象上扩展。在扩展函数内, 可以通过 this 来判断接收者是否为 NULL,这样,即使接收者为 NULL,也可以调用扩展函数。例如:
fun Any?.toString(): String {
if (this == null) return "null"
// 空检测之后,“this”会自动转换为非空类型,所以下面的 toString()
// 解析为 Any 类的成员函数
return toString()
}
fun main(arg:Array<String>){
var t = null
println(t.toString())
}
看一下isNulOrEmpty函数的实现:
@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}
可以看到,当被调用对象是NULL时它返回true。并且返回类型Boolean是一个非空对象,所以isNullOrEmpty函数无论如何要么返回true要么返回false不可能是NULL。
扩展在kotlin的应用无处不在,像let
、also
、lifelycleScope
、viewModelScope
等等都是扩展。
扩展这么神奇,那它到底是何物呢?从kotlin官方文档来看,也看不出个究竟:
https://book.kotlincn.net/text/extensions.html
官方文档只说了它的特点,并没有深入介绍:
扩展是静态解析的
扩展不能真正的修改他们所扩展的类。通过定义一个扩展,并没有在一个类中插入新成员, 只不过是可以通过该类型的变量用点表达式去调用这个新函数。
笔者动手写了一个demo,从字节码指令窥探一下它的原理。
Kotlin扩展函数和扩展属性的原理
在com.devnn.bean
目录下新建一个名为MyExtension.kt
的文件,内容如下:
package com.devnn.bean
import com.devnn.demo.MainActivity
//扩展属性
val MainActivity.myExtProperty: String
get() {
val data: String? = this.getData()
return "#${data}#"
}
//扩展函数
fun MainActivity.myExtFun(): String {
return "MainActivityExtFun:${this.javaClass.simpleName}"
}
上面的扩展属性访问了MainActivity
的getData
方法:
//com.devnn.demo.MainActivity
fun getData():String{
return "dataFromMainActivity"
}
在MainActivity
中去访问这个扩展属性和扩展函数:
//com.devnn.demo.MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.i("MainActivity", "onCreate")
Log.i("MainActivity", "myExtensionProperty=${myExtProperty}")
Log.i("MainActivity", "myExtensionFun=${myExtFun()}")
}
运行打印日志如下:
2022-11-07 22:53:49.431 2737-2737/com.devnn.demo I/MainActivity: onCreate
2022-11-07 22:53:49.432 2737-2737/com.devnn.demo I/MainActivity: myExtensionProperty=#dataFromMainActivity#
2022-11-07 22:53:49.432 2737-2737/com.devnn.demo I/MainActivity: myExtensionFun=MainActivityExtFun:MainActivity
在build目录中查看MyExtension.kt
被编译成了一个class文件,文件名是MyExtensionKt.class
,查看它的字节码内容如下:
// class version 52.0 (52)
// access flags 0x31
public final class com/devnn/bean/MyExtensionKt {
// compiled from: MyExtension.kt
@Lkotlin/Metadata;(mv={1, 6, 0}, k=2, xi=48, d1={"\u0000\u000e\n\u0000\n\u0002\u0010\u000e\n\u0002\u0018\u0002\n\u0002\u0008\u0004\u001a\n\u0010\u0005\u001a\u00020\u0001*\u00020\u0002\"\u0015\u0010\u0000\u001a\u00020\u0001*\u00020\u00028F\u00a2\u0006\u0006\u001a\u0004\u0008\u0003\u0010\u0004\u00a8\u0006\u0006"}, d2={"myExtProperty", "", "Lcom/devnn/demo/MainActivity;", "getMyExtProperty", "(Lcom/devnn/demo/MainActivity;)Ljava/lang/String;", "myExtFun", "app_debug"})
// access flags 0x19
public final static getMyExtProperty(Lcom/devnn/demo/MainActivity;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "<this>"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 8 L1
ALOAD 0
INVOKEVIRTUAL com/devnn/demo/MainActivity.getData ()Ljava/lang/String;
ASTORE 1
L2
LINENUMBER 9 L2
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
BIPUSH 35
INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
BIPUSH 35
INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L3
LOCALVARIABLE data Ljava/lang/String; L2 L3 1
LOCALVARIABLE $this$myExtProperty Lcom/devnn/demo/MainActivity; L0 L3 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x19
public final static myExtFun(Lcom/devnn/demo/MainActivity;)Ljava/lang/String;
@Lorg/jetbrains/annotations/NotNull;() // invisible
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 0
LDC "<this>"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 13 L1
LDC "MainActivityExtFun:"
ALOAD 0
INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class;
INVOKEVIRTUAL java/lang/Class.getSimpleName ()Ljava/lang/String;
INVOKESTATIC kotlin/jvm/internal/Intrinsics.stringPlus (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
ARETURN
L2
LOCALVARIABLE $this$myExtFun Lcom/devnn/demo/MainActivity; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
可以看到kotlin的扩展属性和扩展函数其实是类的静态方法。
被扩展类是这个静态方法的第一个实参。
这么一看,就明白了。说到底,它们只是kotlin的一个语法糖而已。
这么说扩展要访问被扩展类的方法,只能访问public类型的成员?经验证,笔者将MainActivity
的getData
访问改成private编译就会报错。
再看看MainActivity
中的调用扩展的字节码:
L2
LINENUMBER 77 L2
LDC "MainActivity"
LDC "myExtensionProperty="
ALOAD 0
INVOKESTATIC com/devnn/bean/MyExtensionKt.getMyExtProperty (Lcom/devnn/demo/MainActivity;)Ljava/lang/String;
INVOKESTATIC kotlin/jvm/internal/Intrinsics.stringPlus (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L3
LINENUMBER 78 L3
LDC "MainActivity"
LDC "myExtensionFun="
ALOAD 0
INVOKESTATIC com/devnn/bean/MyExtensionKt.myExtFun (Lcom/devnn/demo/MainActivity;)Ljava/lang/String;
INVOKESTATIC kotlin/jvm/internal/Intrinsics.stringPlus (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
观察调用方的字节码会发现调用扩展属性或者扩展方法,其实都是在调用静态方法,将this
(被扩展对象)传给了这个静态方法作为实参,这样扩展就可以访问被扩展类中的成员。
关于扩展就介绍到这了。