最近观察到的Kotlin的华点

Author Avatar
Dexlind 10月 18, 2017
  • 在其它设备中阅读本文章

不定期更新。

以下内容使用的 Kotlin 版本为 1.1.51。

2017-10-18

封面换成了艾拉酱,真好啊~真好啊~ ヾ(≧▽≦*)o

一件Java能做但是Kotlin不能做的事情

在Codewars上刷题的时候发现的,本来这网站上面的Kotlin题就少,而且还有大把大把不会写,能玩的就更少了。这种题目不调戏一下冰酱就可惜了。具体是这道题

这题要求你代码中不能出现fun{以及}的情况下让定义一个拼接两个字符串的函数。这个要求等同于禁止你使用lambda表达式、函数声明,并且限制你仅用一个表达式完成任务(根据我的理解,是这样的没错)。

那我们来找找有什么现成的有invoke()方法的东西吧。首先想到了反射,但是反射那家伙需要传入接收者作为第一个参数,rua~。既然反射不行,啊,有个更好的东西,看起来完美符合需求。

马上写好代码准备吊打冰酱,然后就是喜闻乐见的辣鸡Kotlin时间,代码如下:

val concatString = MethodHandles.lookup().findVirtual(String::class.java,
            "concat", MethodType.methodType(String::class.java, String::class.java))

val str = concatString("我永远喜欢", "珂朵莉") as String // Boom!

然后在Java里写下同样的代码进行测试,嗯,果然是辣鸡Kotlin。

炸裂原因是Kotlin不支持所谓的签名多态性(signature polymorphism),从编译出来的字节码可以看得出来:

// kotlin
INVOKEVIRTUAL java/lang/invoke/MethodHandle.invoke ([Ljava/lang/Object;)Ljava/lang/Object;

// java
INVOKEVIRTUAL java/lang/invoke/MethodHandle.invoke (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

运行时要成功调用该方法的话,字节码中的参数与返回值类型必须与运行时MethodHandle所代表的实际的方法类型一致,否则会抛出WrongMethodTypeException。上面那段Kotlin代码运行时就会抛出这个异常,异常信息为cannot convert MethodHandle(String,String)String to (Object[])Object,正好与上面的字节码相对应。嗯就是这样。

Kotlin编译器现在还不能支持多态签名,而且这将持续很长一段时间(官方人员说了,1.2版本也不会实现这项功能)。

解决方法:辣鸡Kotlin。

2017-10-10

最近翻看以前写的旧代码,发现由于Kotlin及其IDEA插件版本更新了,静态检查工具功能增强,检出了一些令人窒息的代码(例如 Check for instance is always ‘true’)。然后我在查看这些乱七八糟的代码时发现了两个问题。

编译器生成了无用的字节码(?)

把问题单独抽出来就像是这样:

class A<T : Any>(val data: T)
class B(val string: String)

fun test(a: A<*>) {
    a.data as B   // 注意这行对应的字节码
    a.data.string // 这里 a.data 有提示 Smart cast to B
}

然后看看编译出的字节码里面都有啥:

// 以上省略
   L1
    LINENUMBER 5 L1
    ALOAD 0
    INVOKEVIRTUAL A.getData ()Ljava/lang/Object; // a.data
    DUP
    IFNONNULL L2   // <============================ 这里进行判空
    NEW kotlin/TypeCastException
    DUP
    LDC "null cannot be cast to non-null type B"
    INVOKESPECIAL kotlin/TypeCastException.<init> (Ljava/lang/String;)V
    ATHROW
   L2
    CHECKCAST B    // <============================ 不为空则进行类型转换
    POP
   L3
// 以下省略

大致内容就是判断 a.data 是否为 null,如果是 nullthrow TypeCastException("null cannot be cast to non-null type B")。然而根据 A 这个类的定义,其属性 data 必定是非空的。对非空属性进行 null check 岂不是多余?难道说编译器太傻,在分析这种操作时不知道 a.data 是非空的?

解决方法:加个问号,如下所示:

fun test(a: A<*>) {
    a.data as B?   // 加了个问号,变成了B?
    a.data.string  // 注意这里 a.data 同样有提示 Smart cast to B
}

这时候字节码就是这样的:

// 以上省略
   L1
    LINENUMBER 5 L1
    ALOAD 0
    INVOKEVIRTUAL A.getData ()Ljava/lang/Object; // a.data
    CHECKCAST B    // <============================ 直接进行类型转换
    POP
   L2
// 以下省略

这回就没有多余的判空处理了。但是注意代码中的 a.data.string,IDE提示 a.data 能够 Smart cast to B。咦编译器还是蛮聪明的呢!

检查工具提示可对代码进行负优化(?)

这个问题单独抽出来就像这样:

class A
class B {
    fun getA(): A = TODO()
}

fun test(some: Any?): A {
    return if (some is B) some.getA() else A() // 注意这行
}

然后你可以看到,IDE提示你可以 Replace ‘if’ expression with elvis experssion。如果你让IDE帮你优化,就变成了这样:

fun test(some: Any?): A {
    return (some as? B)?.getA() ?: A() // WTF???
}

不知道你们是怎么想的,至少对于我来说可读性下降,而且很明显生成的字节码也变复杂了。明显是负优化,吧?

解决方法:@Suppress("IfThenToElvis")

很久以前的冷饭

把以前想过的东西拿出来凑字数。

Kotlin数组类型与reified

在Java,你可以这样写:

public static void some(Object o) {
    if (o instanceof String[]) {
        System.out.println("this is String[]");
    }
}

但是辣鸡Kotlin不行:

fun some(any: Any?) {
    // 编译错误:Cannot check for instance of erased type: Array<String>
    //               ↓
    if (any is Array<String>) {
        println("this is String[]")
    }
}

说好的 Array<reified T> 呢?

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2017/10/18/huadian/