Kotlin的语法糖太甜啦——开发MC插件遇到的坑

最近在学习使用Kotlin开发PaperMC插件,遇到了一个大坑,不吐不快。

PersistentDataType<T, Z> 接口

我们可以给物品或方块添加自定义的NBT标签,而这个接口定义的自定义标签的数据类型,泛型T表示标签中实际所存储的数据的类型,泛型Z表示读取后转化为的数据类型。

比如我希望添加一个标签用于记录最近更新标签的时间,以Long型时间戳的方式存储,可以创建一个类实现该接口,有如下代码:

object DatetimeTagType : PersistentDataType<Long, ZonedDateTime> {
    override fun getPrimitiveType(): Class<Long> {
        return Long::class.java
    }
    override fun getComplexType(): Class<ZonedDateTime> {
        return ZonedDateTime::class.java
    }
    override fun toPrimitive(complex: ZonedDateTime, context: PersistentDataAdapterContext): Long {
        return complex.toInstant().toEpochMilli()
    }
    override fun fromPrimitive(primitive: Long, context: PersistentDataAdapterContext): ZonedDateTime {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(primitive), ZoneId.systemDefault())
    }
}

然后通过下面的代码向主手的物品存储一条自定义标签信息:

val timeNamespace = NamespacedKey(HelloPaper.getInstance(), "updateTime")
player.inventory.itemInMainHand.itemMeta.persistentDataContainer.set(
    timeNamespace,//命名空间
    DatetimeTagType,//自定义NBT数据类型
    ZonedDateTime.now()//当前时间
)

向物品存储自定义的时间信息时,底层会调用toPrimitive方法,将ZonedDateTime转换为Long类型存储起来。

读取数据也很简单:

player.inventory.itemInMainHand.itemMeta.persistentDataContainer.get(
    timeNamespace,//命名空间
    DatetimeTagType//自定义NBT数据类型
)

 底层通过调用fromPrimitive方法将Long型数据转换为ZonedDateTime

现在来测试一下存储数据。一切正常:

 那么读取数据呢?

Caused by: java.lang.IllegalArgumentException: The found object is of the type Long. Expected type long
    at org.bukkit.craftbukkit.v1_19_R1.persistence.CraftPersistentDataTypeRegistry.extract(CraftPersistentDataTypeRegistry.java:259) ~[paper-1.19.2.jar:git-Paper-237]
    ...

 哦吼,完蛋!居然报错了。

错误分析

翻译一下报错信息:object的类型是Long,期望的是long

这就很奇怪了。这里的object应该就是存储的时间戳,它确实是Long类型,但是插件不知道为什么偏偏需要long类型。我尝试将Kotlin改为Java,一切正常没有报错!

好吧,不知道Kotlin搞了什么鬼。观察DatetimeTagType的代码,fromPrimitive函数需要Long类型的参数,看上去没有问题。既然报错发生在 paper.jar 中,那我就尝试找找源代码。通过Google搜索CraftPersistentDataTypeRegistry类,果然让我找到了Bukkit的一些源码。根据异常抛出的类与行号,我找到了异常发生的位置。

org/bukkit/craftbukkit/persistence/CraftPersistentDataTypeRegistry.java

public <T> T extract(Class<T> type, NBTBase tag) throws ClassCastException, IllegalArgumentException {
    TagAdapter adapter = this.adapters.computeIfAbsent(type, CREATE_ADAPTER);
    if (!adapter.isInstance(tag)) {
        throw new IllegalArgumentException(String.format("`The found tag instance cannot store %s as it is a %s", type.getSimpleName(), tag.getClass().getSimpleName()));
    }
    Object foundValue = adapter.extract(tag);
    if (!type.isInstance(foundValue)) {
        throw new IllegalArgumentException(String.format("The found object is of the type %s. Expected type %s", foundValue.getClass().getSimpleName(), type.getSimpleName()));
    }//foundValue是Long,type是long
    return type.cast(foundValue);
}

foundValue就是从NBT标签中取得的Long型时间戳,此处是Object类型,需要转为实际类型。type应当是包装类型Long的class,实际却是基本类型long的class。因为任何对象都不可能是基本类型的实例,因此type.isInstance(foundValue)判断为false,进而引发异常。

那么这个type又是从哪来的呢?

 org/bukkit/craftbukkit/persistence/CraftPersistentDataContainer.java

@Override
public <T, Z> Z get(NamespacedKey key, PersistentDataType<T, Z> type) {
    Validate.notNull(key, "The provided key for the custom value was null");
    Validate.notNull(type, "The provided type for the custom value was null");
    NBTBase value = this.customDataTags.get(key.toString());
    if (value == null) {
        return null;
    }
    return type.fromPrimitive(registry.extract(type.getPrimitiveType(), value), adapterContext);
}

 从该方法的最后一行调用了extract方法,extract()第一个参数通过type.getPrimitiveType()得到。由此推断出,我所编写的getPrimitiveType函数,返回了long.class而非java.lang.Long.class。不用想,这肯定是Kotlin干的好事。谁能想到

return Long::class.java

返回的是基本类型long的class对象而非包装类型Long的class对象。我打开Kotlin字节码分析工具,然后反编译为java,看到这行代码被编译成这样:

return Long.TYPE;

那么这个Long.TYPE又是什么呢?查看java的源码:

/**
 * The {@code Class} instance representing the primitive type
 * {@code long}.
 *
 * @since   1.1
 */
@SuppressWarnings("unchecked")
public static final Class<Long> TYPE = (Class<Long>) Class.getPrimitiveClass("long");

原来Long.TYPE就是基本类型long的class对象。可是Kotlin为什么就这么做?

问题解决

根据Kotlin的中文文档,Java 的原生类型映射到了相应的 Kotlin 类型。我在Kotlin中使用非空的Long类型,也就是kotlin.Long,实际上对应Java中的基本类型long;而可空的kotlin.Long?才对应java.lang.Long

  • 那我将DatetimeTagType的第一个泛型参数改为Long?是否可行呢?答案是不行(也可能是我不知道)!这样的话getPrimitiveType()返回类型变成了Class<Long?>,我不知道如何返回这样一个Class对象。还像之前那样return Long::class.java的话,会提示类型不匹配,因为Class<Long>不是Class<Long?>
  • 如果写成return Long?::class.java呢?似乎没有这种写法,编译器还是报错(初学Kotlin,还不能完全理解所有的语法)。
  • 如果第一个泛型参数明确写为java.lang.Long呢?这样勉强可以解决,只需要在特定的位置将kotlin.Lang强转为java.lang.Long或者将java.lang.Long强转为kotlin.Lang即可。但这样写出来的代码比较丑陋,会变成下面这幅模样:
object DatetimeTagType : PersistentDataType<java.lang.Long, ZonedDateTime> {
    override fun getPrimitiveType(): Class<Long> {
        return java.lang.Long::class.java
    }
    override fun getComplexType(): Class<ZonedDateTime> {
        return ZonedDateTime::class.java
    }
    override fun toPrimitive(complex: ZonedDateTime, context: PersistentDataAdapterContext): Long {
        return complex.toInstant().toEpochMilli() as Long
    }
    override fun fromPrimitive(primitive: Long, context: PersistentDataAdapterContext): ZonedDateTime {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(primitive as kotlin.Long), ZoneId.systemDefault())
    }
}

 有没有更优雅的方法?回过头来看,我只要让getPrimitiveType()函数返回JVM平台类型java.lang.Long.class就可以了,于是我手动指定返回java.lang.Long::class.java。这样返回类型不匹配,那就再进行一次强转。最终该方法变成这样:

@Suppress("UNCHECKED_CAST")
override fun getPrimitiveType(): Class<Long> {
    return java.lang.Long::class.java as Class<Long>
}

重新编译运行,终于正常了!

被这样一个小问题折磨了很久。你根本想象不到Kotlin究竟会怎样编译你的代码。

posted @ 2022-10-30 17:20  Fortern  阅读(229)  评论(0编辑  收藏  举报