Kotlin语法基础篇十五::: 运算符

743 阅读5分钟

前言

在上一篇文章中我们介绍了Kotlin中集合的使用,这篇文章我们来介绍Kotlin中一个比较常用的运算符 ::

1.获取类引用

相信使用Kotlin开发Android的读者,对于下面这行代码都会很熟悉:

val TAG  = MainActivity::class.simpleName

获取 Kotlin 类的运行时引用是Kotlin反射中最基本的功能,它的语法也比较简单:

ClassName::class

在类名后添加::class这意味着我们获取指定类的类引用,在Kotlin中该引用是KClass类型的值。如果要获取Java的类引用,我们需要在KClass示例上加上.java属性。KClassKotlin中为我们提供的一个获取运行时类信息的类,该类包含了我们在运行时所需要的各种属性,如下代码示例:

public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {
    
    public actual val simpleName: String?

    public actual val qualifiedName: String?

    public val constructors: Collection<KFunction<T>>

    public actual fun isInstance(value: Any?): Boolean
    
    // ...省略
}    

这里为了方便阅读,对源码进行了一些删减。只保留了几个常见的运行时属性和一个isInstance()方法。我们知道Kotlin代码最终还是要编译成Java字节码的,下面我们就来看一下这行代码在Java中是如何实现的:

val TAG  = MainActivity::class.simpleName

在Android Studio中依次打开Tools -> Show Kotlin Bycode ->在右边的弹出框中我们点击Decopile按钮:

public static final void main() {
   String TAG = Reflection.getOrCreateKotlinClass(MainActivity.class).getSimpleName();
}

接着我们顺着getOrCreateKotlinClass()方法依次查看其调用链:

public static KClass getOrCreateKotlinClass(Class javaClass) {
    return factory.getOrCreateKotlinClass(javaClass);
}

public KClass getOrCreateKotlinClass(Class javaClass) {
    return new ClassReference(javaClass);
}

public class ClassReference(override val jClass: Class<*>) : KClass<Any>, ClassBasedDeclarationContainer {
    override val simpleName: String?
        get() = getClassSimpleName(jClass)
    ...    
}

public fun getClassQualifiedName(jClass: Class<*>): String? = when {
    ...
    else -> classFqNames[jClass.name] ?: jClass.canonicalName
}

canonicalName属性就是通过JavaClass对象的getSimpleName()方法来获取的。到这里我们可以看到KClass对象在编译期间还是通过Java中的Class对象来间接帮助我们获取运行时信息。

2.可调用的引用

函数、属性以及构造函数的引用,还可以用于调用或者用作函数类型的实例。所有可调用引用的公共超类型是KCallable<out R>,其中R是返回值类型,对于属性是属性类型,对于构造函数是所构造类型。

3.属性引用

在介绍by关键字的文章中,我们在使用属性委托时,在代理类Delegate中的setValue()方法就要求我们必须要传被代理属性的引用,下面我们再来看下该方法的实现:

class Delegate {
    operator fun getValue(thisRef:Any?, prop:KProperty<*>) : String {
        return "$thisRef, name = ${prop.name}"
    }
}

这里的KProperty就是我们Kotlin中封装的一个存储属性相关信息的接口,它继承自KCallable接口。下面我们来看一个简单的例子:

val name: String get() = "name"

fun main() {
    val property = ::name
    println(property.name)
}

反编译成Java的代码如下:

final class ReferenceClass extends PropertyReference0Impl {
   public static final KProperty0 INSTANCE = new  ReferenceClass();

    ReferenceClass() {
      super(ReferenceKt.class, "name", "getName()Ljava/lang/String;", 1);
   }

   public Object get() {
      return ReferenceKt.getName();
   }
}

public final class ReferenceKt {
   public static final String getName() {
      return "name";
   }

   public static final void main() {
      KProperty0 property = ReferenceClass.INSTANCE;
      String var1 = property.getName();
      System.out.println(var1);
   }
}

为了方便阅读,这里对反编译的代码做了一些调整。PropertyReference0Impl这个类实现了 KProperty接口。在mian()函数中我们创建了ReferenceClass对象,然后获取name属性的值。到这里我们可以看到一个属性引用它真实的实现过程。

4.函数引用

在前面的文章中我们介绍到在Kotlin中我们使用Lambda表达式来初始化一个函数类型,也就是说Lambda表达式是函数类型的实例。今天我们来介绍另外一种获取函数类型实例的方法,使用 :: 运算符。下面我们就来看一个简单的示例:

fun main() {
    val refMethod = ::test
    normal(refMethod)
    
    val block: () -> Unit = { println("block called.") }
    normal(block)
}

inline fun normal(block: () -> Unit) {
    block.invoke()
}

fun test() {
    println("test method called.")
}

反编译后获取到Java代码如下:

public final class ReferenceKt {
   public static final void main() {
      KFunction refMethod = null.INSTANCE;
      int $i$f$normal = false;
      ((Function0)refMethod).invoke();
      
      Function0 block = (Function0)null.INSTANCE;
      int $i$f$normal = false;
      block.invoke();
   }

   public static void main(String[] var0) {
      main();
   }

   public static final void normal(@NotNull Function0 block) {
      int $i$f$normal = 0;
      Intrinsics.checkNotNullParameter(block, "block");
      block.invoke();
   }

   public static final void test() {
      String var0 = "test method called.";
      System.out.println(var0);
   }
}

由反编译后的结果我们可以看到,获取类引用是通过创建KFunction接口的子类对象。而Lambda表达式则是创建了Function0接口的子类对象。KFunction接口实现了KCallable接口,而Function0接口是Lambda表达式在Java中的实现方式。

5.构造函数的引用

通常我们获取一个Fragment的对象,都会使用如下方式:

class MainFragment private constructor() : Fragment() {

    companion object {
        fun getInstance() = MainFragment()
    }

}

MainFragment的构造函数,它也是一个函数类型,等价于:

val block: () -> MainFragment

我们可以使用 :: 运算符先来获取该构造函数的实例,然后通过调用invoke()方法来获取MainFragment的实例:

val instance = ::MainFragment

注意这里的instance是函数类型 KFunction0<MainFragment> 的引用,我们要想获取MainFragment的实例,还得调用该函数类型的invoke()方法。

val instance = ::MainFragment.invoke()

虽然这种写法看上去有些多余,但是我们也要了解一下使用这种获取构造函数引用的方式来创建一个对象。

总结

关于 :: 运算符的应用到这里就介绍完了。这种通过获取属性、方法、构造函数引用的方式虽然看上去不太好理解。但是在实际开发中我们常常还是会遇到,所以我们还是要去理解这种方式。到这里关于Kotlin中基础知识的介绍也算是结尾了。我们下期再见~