Kotlin - delay initialization and sealing classes

1, lateinit delay initialization keyword

Many syntax features in Kotlin, such as variable immutability, variable non nullability, and so on, are designed to ensure program safety as much as possible. For example, there are many global variable instances in your class. In order to ensure that they can meet Kotlin's null pointer check statement standard, you have to make non null judgment protection, even if you are very sure that they will not be null.

Take a look at the following distance:

class MainActivity : AppCompatActivity() {

    private var s: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        s = "test"

        Log.d("TAG", "onCreate: ${s!!.length}")


    }


}

We set s as a global variable, but its assignment is done in the onCreate() method, so we have to assign s to null.

Although you are sure that s has been assigned successfully before printing, the length of printing s still needs to be judged empty, otherwise the compilation fails.

When there are more and more instances of global variables in your code, this problem will become more and more obvious. At that time, you may have to write a lot of additional code for space determination, but only to meet the compilation requirements of Kotlin.

Fortunately, there is a solution to this problem, and it is very simple, that is, to delay the initialization of global variables.

The keyword lateinit used in initialization can be used by the high-speed compiler. I will initialize this variable later, so that I don't have to assign null at the beginning.

class MainActivity : AppCompatActivity() {

    private lateinit var s: String

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        s = "test"

        Log.d("TAG", "onCreate: ${s.length}")


    }


}


It can be seen that with the lateinit keyword, there is no need to assign null at the beginning, and there is no need to judge the blank when printing the length. Of course, using the lateinit keyword is not without risk. If there is no assignment, the program will crash and throw exceptions.

In addition, we can judge whether the initialization of global variables has been completed through the code, so that we can effectively avoid repeated initialization of a certain variable at some time.

class MainActivity : AppCompatActivity() {

    private lateinit var s: String

    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (!::s.isInitialized) {
            s = "test"
        }

        Log.d("TAG", "onCreate: ${s.length}")


    }


}

2, Optimize code with sealed classes

Create a new Kotlin file with the following code:

interface Result

class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

Here, a Result interface is defined to represent the execution Result of an operation. Nothing is written in the interface. Then two classes are defined to implement the Result interface: a Success class is used to represent the Result of Success, and a Failure class is used to represent the Result of Failure.

Next, define a getResultMsg() method to get the information of the final execution result. The code is as follows:

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException()
}
 

getResultMsg() method accepts a Result parameter. We judge by the when statement. If the Result belongs to Success, it will return a Success message. If the Result is Failure, it will return an error message. So far, the code has no problem. What's annoying is that we have to write an else condition statement, otherwise Kotlin compilation fails. In fact, the code will never go to else because there are only two types, Just to satisfy Kotlin's grammar.

In addition, there is a potential risk in writing else conditions. If we add an Unknown class and implement the Result interface to represent the Unknown execution results, but forget to add the corresponding conditions in the getResultMsg() method, the compiler will not remind us, but directly enter the else conditions, throw exceptions from them and cause the program to crash.

Kotlin's sealed class solves this problem well. The keyword of the sealed class is sealed class, and its usage is also very simple. We can easily transform the Result interface into a sealed class writing method:

sealed class Result

class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()


fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
}

There is no change in the code, but the interface is changed to sealed class. A sealed class is an inheritable class, so you need to add a pair of parentheses when inheriting it.

The advantage of the sealed class is that the else condition statement can be cancelled in the getResultMsg() method.

Why can we compile without else conditional statements? The Kotlin compiler will automatically check the subclasses of the sealed class and force all the conditions corresponding to each subclass to be processed. In this way, it can be ensured that even if there is no else condition, there can be no omission. If you add an Unknown class now and let it inherit from Result, then the getResultMsg() method will definitely report an error, and you must add an Unknown statement condition to compile.

Note: sealed classes and all their subclasses can only define the top-level position of the same file, and cannot be nested in other classes, which is also limited by the underlying implementation mechanism of sealed classes.

Tags: Android kotlin programming language

Posted by Cut on Sun, 31 Jul 2022 22:28:16 +0300