We often assume the state of our program is acceptable when our functions are called. Sometimes we assume the state or input can be invalid, so we do something like the following:
fun doThing(foo: Bar?) {
if (foo == null) {
return
}
.... // what you meant to do
}
Both these assumptions can lead to hard-to-find errors and unreadable code. Fortunately, the Kotlin standard library provides three basic functions for ensuring everything is in order:
- Require: My function needs parameters to be valid in this way. Throw an
IllegalArgumentException
when false. - Check: My function's dependency is in the correct state to be called. Throw an
IllegalStateException
when false. - Assert: My function's result is valid. Throw an
AssertionError
when false. Also, runtime assertions have to be enabled on the JVM with-ea
or enabled with Native.
In addition to validation of a condition, each one of these functions has an optional lazy argument for an error message if evaluated to false. All three runtime check APIs are the same. Here is an example from require
.
fun require(value: Boolean)
inline fun require(value: Boolean, lazyMessage: () -> Any)
Let's take a look at each runtime check in depth, then write code to fire a laser cannon
Require
Require
is used for parameter validation. It ensures its parameter is in a valid state.
fun fireLaser(cannonNumber: Int) {
require(cannonNumber >= 0) { "Must pass a valid cannon number: $cannonNumber < 0" }
.... // fire laser beams!!!
}
Check
Check
makes sure our function's dependencies can be called safely. It is used to verify everything is ready to go.
fun fireLaser() {
// setup above
check(laserCannonManager.isReadyToFire) { "You must arm the lasers before firing!" }
.... // fire laser beams!!!
}
Assert
Assert
does the last bit of validation before a function is done or returns. It is your last chance before handing control to another part of the program.
fun fireLaser() {
// setup above
val fired = laserCannon.fire()
assert(fired == true) { "cannon did not fire successfully" }
}
Why runtime checks?
They provide runtime assurance which can be useful in addition to unit tests. The checks force the program to fail fast and give the developer more info than a stacktrace from a distant failed component.
Obviously, these checks shouldn't be everywhere and should be added judiciously. Crucial functions which exist in a complicated space would benefit the most. See below how all three runtime checks ensure proper usage of our core laser cannon functionality.
fun fireLaser(cannonNumber: Int) {
// first, validate parameters
require(cannonNumber >= 0) { "Must pass a valid cannon number"}
// then, verify working dependencies
check(laserCannonManager.isReadyToFire) { "You must arm the lasers before firing!" }
check(guidanceSystem.canLockOn()) { "Cannot lock on target" }
// call laser code safely
laserCannonManager.charge(cannonNumber)
guidanceSystem.lockOn()
val laserCannon = laserCannonManager.cannons[cannonNumber]
val fired = laserCannon.fire() // fire laser beams!!!
// lastly, make sure our cannon fired flawlessly
assert(fired == true) { "cannon did not fire successfully" }
}
Hopefully you have a comfortable understanding of these runtime conditions. Feel free to post any questions you have in the comments!
Did you like what you read? You can also follow me on Twitter for Kotlin and Android content.
Top comments (0)