NoArg 插件的 invokeInitializers


#1

上周的时候 Kotlin 1.3 发布了,由于之前 RC 版本以及 KotlinConf 造势很多,所以正式版出来之后大家就改个版本号,把协程的 experimental 去掉,就差不多了。

上周微信群里有小伙伴说到 NoArg 插件生成的无参构造方法不会初始化类内部定义的属性,例如:

@Poko
data class DontDoThis(val requiredProperty: String,
                      val invalidDefaultValue: Int = 1000) {
    val wontBeInitialized by lazy {
        "HelloWorld"
    }
    
    val wontBeInitialized2 = 2
}

wontBeInitializedwontBeInitialized2 都不会被正常初始化。

  • 对于前者,我们知道编译器要为我们生成一个代理对象,我们访问它时,实际上是访问代理对象来获取对应的值,而代理对象因没有被初始化,导致访问前者时会出空指针。
  • 而后者,本身就是一个整型,不被初始化,访问时就是默认的 0。

前面我们已经有文章提到过这个现象,包括对于 Gson 反序列化数据类的时候出现的种种问题中,也有些与此有关。可以参考相关文章:

其实 NoArg 的配置还有一个叫 invokeInitializers 的家伙,你可以这么配置:

noArg{
    invokeInitializers = true
    annotation "com.bennyhuo.annos.Poko"
}

它是什么意思呢?对于前面的那个类,这个配置为 true 之后,生成的无参构造器就会大致相当于:

public DontDoThis(){
    super();
    wontBeInitialized$Delegate = new .... ;
    wontBeInitialized2 = 2;
}

而不加这个配置的话,就是这样:

public DontDoThis(){
    super();
}

看来这个配置还是很有用的。

过去我之前好几次看到它,并尝试配置,结果用 IntelliJ Kotlin 插件自带的 “Show Kotlin Bytecode” 看了之后,发现生成的构造器没有任何变化:

public <init>()V
  L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
  L1
    LOCALVARIABLE this Lkotlin/Unit; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

这让人相当的困惑,我一直以为这个配置没啥用。前几天在看 NoArg 插件的源码时看到这个配置,试了下还是有用的,估计是 “Show Kotlin Bytecode” 没有根据 Gradle 当中的配置来编译导致的吧,大家不用在意了,我们可以在 IntelliJ 里面下载其他看字节码的插件,其实是可以看到

// access flags 0x1
public <init>()V
   L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 6 L1
    ALOAD 0
    GETSTATIC com/bennyhuo/DontDoThis$wontBeInitialized$2.INSTANCE : Lcom/bennyhuo/DontDoThis$wontBeInitialized$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD com/bennyhuo/DontDoThis.wontBeInitialized$delegate : Lkotlin/Lazy;
   L2
    LINENUMBER 10 L2
    ALOAD 0
    ICONST_2
    PUTFIELD com/bennyhuo/DontDoThis.wontBeInitialized2 : I
    RETURN
   L3
    LOCALVARIABLE this Lkotlin/Unit; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

我们通过 Bytecode Viewer 的功能看到生成的无参构造方法的字节码其实是这样的,其中明显有对类内部的属性初始化的操作。

既然这个配置这么有用,为什么 Kotlin 官方把它默认关闭了?大约是因为 1.1.3 这个版本刚刚带上这个功能,当时因为有一些小问题,大家抱怨升级之后导致代码无法编译通过,影响太大,后来尽管问题已经在 1.1.3-2 修复,但这个可能影响程序结果的配置还是关掉了,这也是为了稳定性考虑,如果大家有明确的需要,还是自己手动打开吧。


转载请注明出处:微信公众号 Kotlin


#2

大佬 想请教一个问题,在Kotlin中@Parcelize只能序列化构造方法里定义的成员,不在构造器中的成员无法被序列化,但是我这个成员可能在构造的时候还不能拿到正确逻辑值,所以觉得放到构造器中不合适。大佬有什么好的实践方案吗


#3

序列化只应该保存单纯的数据,应当尽量把逻辑计算部分与需要保存的状态分离。

  • 如果一个类要作为数据被序列化,那么尽量不要在类当中加入逻辑
  • 对于一个类来说,如果它在构造的时候不能把所有状态获得,那么考虑延迟这个类的初始化,或者用 Builder 模式对它进行构造。

京ICP备16022265号-2 Kotlin China 2017 - 2018
本站由腾讯云提供计算服务