简体   繁体   中英

Why is the no lateinit block in Kotlin?

The following code is valid Kotlin code:

abstract class A {

    protected lateinit var v: X

    abstract fun f(): X

    class SubA : A() {
        override fun f(): X {
            return SubX()
        }

        init {
            v = f()
        }
    }
}

It defines an abstract class which has a lateinit var field and an abstract method that sets the value of that field. The reason behind this is that that method may be called later again, and its behavior should be defined in the subclasses that extend the original class.

This code is a simplification of a real-world code, and even though it works, I feel like it is messy since the developer of the subclass could choose not to (or forget) to call v = f() inside an init block. And we cannot do that in A either because then it will show a warning that we are calling a non-final method in the constructor. What I propose is the following:

abstract class A {

    private lateinit var v: X

    abstract fun f(): X

    class SubA : A() {
        override fun f(): X {
            return SubX()
        }
    }

    lateinit { // this does not exist
        v = f()
    }
}

The benefits of this is that now the field can be private instead of protected , and the developer does not have to manually call v = f() in each of their subclasses (or the subclasses of their subclasses), and the naming fits with the nomenclature of Kotlin since lateinit is already a keyword and init is already a block. The only difference between an init and a lateinit block would be that the contents of a lateinit block are executed after the subclass constructors, not before like init .

My question is, why isn't this a thing? Is this already possible with some other syntax that I do not know about? If not, do you think it's something that should be added to Kotlin? How and where can I make this suggestion so that the developers would most likely see it?

There are three options, and you can implement your lateinit block in two ways

  1. don't lazy init - just have a normal construction parameter
  2. use a delegated lazy property
  3. add a lambda construction parameter to the superclass class A

All of these solves the problem of requiring subclasses of A having to perform some initialization task. The behaviour is encapsulated within class A .

Normal construction parameter

Normally I'd prefer this approach, and don't lazy init. It's usually not needed.

abstract class A(val v: X)

class SubA : A(SubX())

interface X
class SubX : X

fun f() can be replaced entirely by val v .

This has many advantages, primarily that it's easier to understand, manage because it's immutable, and update as your application changes.

Delegated lazy property

Assuming lazy initialization is required, and based on the example you've provided, I prefer the delegated lazy property approach.

The existing equivalent of your proposed lateinit block is a lazy property .

abstract class A {
  protected val v: X by lazy { f() }

  abstract fun f(): X
}

class SubA : A() {
  override fun f(): X {
    return SubX()
  }
}

interface X
class SubX : X

The superclass can simply call the function f() from within the lazy {} block.

The lazy block will only run once, if it is required.

Construction parameter

Alternatively the superclass can define a lambda as construction parameter, which returns an X .

Using a lambda as a construction parameter might be preferred if the providers are independent of implementations of class A , so they can be defined separately, which helps with testing and re-used.

fun interface ValueProvider : () -> X

abstract class A(
  private val valueProvider: ValueProvider
) {
  protected val v: X get() = valueProvider()
}

class SubA : A(ValueProvider { SubX() })

interface X
class SubX : X

The construction parameter replaces the need for fun f() .

To make things crystal clear I've also defined the lambda as ValueProvider . This also makes it easier to find usages, and to define some KDoc on it.

For some variety, I haven't used a lazy delegate here. Because val v has a getter defined ( get() =... ), valueProvider will always be invoked. But, if needed, a lazy property can be used again.

abstract class A(
  private val valueProvider: ValueProvider
) {
  protected val v: X by lazy(valueProvider)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM