
requireblock - a universal way to specify expectations for arguments.checkblock - a universal way to specify expectations for state.errorfunction - a universal way to signal that application reached an unexpected state.- The Elvis operator with
returnorthrow.
// Part of Stack<T> fun pop(num: Int = 1): List<T> { require(num <= size) { "Cannot remove more elements than current size" } check(isOpen) { "Cannot pop from closed stack" } val ret = collection.take(num) collection = collection.drop(num) return ret }
- Expectations are visible even to programmers who have not read the documentation.
- If they are not satisfied, a function throws an exception instead of leading to unexpected behavior. It is important that these exceptions are thrown before the state is modified because this means we don’t have a situation where only some modifications are applied and others are not. Such situations are dangerous and hard to manage[^pokemon]. Thanks to assertive checks, errors are harder to miss and our state is more stable.
- Code is self-checking to some degree. There is less need for unit-testing when these conditions are checked in the code.
- All checks listed above work with smart casting, therefore less casting is required.
- When you calculate the factorial of a number, you might require this number to be a positive integer.
- When you look for clusters, you might require a list of points to not be empty.
- When you send an email, you might require that the email address is valid.
require function, which checks this requirement and throws IllegalArgumentException if it is not satisfied:fun factorial(n: Int): Long { require(n >= 0) return if (n <= 1) 1 else factorial(n - 1) * n } fun findClusters(points: List<Point>): List<Cluster> { require(points.isNotEmpty()) //... } fun sendEmail(user: User, message: String) { requireNotNull(user.email) require(isValidEmail(user.email)) //... }
require function throws an exception when the predicate is not satisfied. When such a block is placed at the beginning of a function, we know that if an argument is incorrect, the function will stop immediately and the user won’t miss the fact that they are using this function incorrectly. An exception will be clear, unlike a potentially strange result that might propagate a long way until it fails. In other words, when we properly specify our expectations for arguments at the beginning of a function, we can then assume that these expectations will be satisfied.fun factorial(n: Int): Long { require(n >= 0) { "Cannot calculate factorial of $n " + "because it is smaller than 0" } return if (n <= 1) 1 else factorial(n - 1) * n }
require inside init block of data classes. It is used to make sure that constructor arguments are correct, by making it impossible to create invalid instances according to the requirements.data class User( val name: String, val email: String ) { init { require(name.isNotEmpty()) require(isValidEmail(email)) } }
require function is used when we specify requirements for arguments. Another common case is when we have expectations of the current state; in such a case, we can use the check function instead, which throws IllegalStateException.- Some functions might need an object to be initialized first.
- Some actions might be allowed only if the user is logged in.
- Some functions might require an object to be open.
check function:fun speak(text: String) { check(isInitialized) //... } fun getUserInfo(): UserInfo { checkNotNull(token) //... } fun next(): T { check(isOpen) //... }
check function works similarly to require, but it throws an IllegalStateException when the stated expectation is not met. It checks if a state is correct. An exception message can be customized using a lazy message, just like with require. When the expectation is on the whole function, we place it at the beginning, generally after the require blocks. However, some state expectations are local, and check can be used later.assert.require and check have Kotlin contracts that state that when a function returns, its predicate is true after this check.public inline fun require(value: Boolean): Unit { contract { returns() implies value } require(value) { "Failed requirement." } }
Dress. After that, assuming that the outfit property is final, it will be smart casted to Dress.fun changeDress(person: Person) { require(person.outfit is Dress) val dress: Dress = person.outfit //... }
class Person(val email: String?) fun sendEmail(person: Person, message: String) { require(person.email != null) val email: String = person.email //... }
requireNotNull and checkNotNull. They both have the capability to smart cast variables, and they can also be used as expressions to “unpack” variables:class Person(val email: String?) fun validateEmail(email: String) { /*...*/ } fun sendEmail(person: Person, text: String) { val email = requireNotNull(person.email) validateEmail(email) //... } fun sendEmail(person: Person, text: String) { requireNotNull(person.email) validateEmail(person.email) //... }
requireNotNull or checkNotNull we can use the non-null assertion !! operator. This is conceptually similar to what happens in Java: we think something is not null, and an NPE is thrown if we are wrong. The non-null assertion !! is a lazy option. It throws a generic NullPointerException exception that explains nothing. It is also short and simple, which makes it easy to abuse or misuse. The non-null assertion !! is often used in situations where a type is nullable but null is not expected. The problem is that even if this is not currently expected, it almost always can be in the future, and this operator only quietly hides the nullability.maxOrNull to find the biggest one. The problem is that this returns nullable because it returns null when the collection is empty. Only a developer who knows that this list cannot be empty will likely use a non-null assertion !!:fun largestOf(a: Int, b: Int, c: Int, d: Int): Int = listOf(a, b, c, d).maxOrNull()!!
!! can even lead to an NPE in such a simple function. Someone might need to refactor this function to accept any number of arguments, but this person might forget that a collection cannot be empty if we want to use maxOrNull on it:fun largestOf(vararg nums: Int): Int = nums.maxOrNull()!! largestOf() // NPE
null and using a non-null assertion !! is not a good option. It is annoying that we need to unpack these properties every time, and we also block the possibility of these properties actually having a meaningful null in the future:class UserControllerTest { private var dao: UserDao? = null private var controller: UserController? = null @BeforeEach fun init() { dao = mockk() controller = UserController(dao!!) } @Test fun test() { controller!!.doSomething() } }
!! or an explicit error throw, you should assume that it will throw an error one day. Exceptions are thrown to indicate something unexpected and incorrect (Item 7: Prefer a null or a sealed result class result when the lack of a result is possible). However, explicit errors say much more than generic NPEs and they should nearly always be preferred.!! does make sense are mainly a result of interoperability between our code and libraries in which nullability is not expressed correctly. When you interact with an API that is properly designed for Kotlin, this shouldn’t be the norm.!!. This suggestion is rather widely approved by our community; in fact, many teams have a policy to enforce it. Some set the Detekt static analyzer to throw an error whenever it is used. I think such an approach is too extreme, but I do agree that it is often a code smell. It seems that this operator’s appearance is no coincidence. !! seems to be screaming “Be careful” or “There is something wrong here”.!!, we should avoid meaningless nullability. In a case like the one presented above, we should use lateinit or Delegates.notNull. lateinit is good practice when we are sure that a property will be initialized before the first use. We deal with such a situation mainly when classes have a lifecycle, and we set properties in one of the first-invoked methods. For instance, when we set objects in onCreate in an Android Activity, viewDidAppear in an iOS UIViewController, or componentDidMount in a React React.Component.class UserControllerTest { private lateinit var dao: UserDao private lateinit var controller: UserController @BeforeEach fun init() { dao = mockk() controller = UserController(dao!!) } @Test fun test() { controller.doSomething() } }
isInitialized property, so in the above example, I could check if dao is initialized by using ::dao.isInitialized.throw or return on the right side. Such a structure is highly readable and it gives us more flexibility in deciding what behavior we want to achieve. First of all, we can easily stop a function using return instead of throwing an error:fun sendEmail(person: Person, text: String) { val email: String = person.email ?: return //... }
null, we can always add these actions by wrapping return or throw into the run function. Such a capability might be useful if we need to log why a function was stopped:fun sendEmail(person: Person, text: String) { val email: String = person.email ?: run { log("Email not sent, no email address") return } //... }
return or throw is a popular and idiomatic way to specify what should happen in the case of variable nullability, and we should not hesitate to use it. Again, if possible, keep such checks at the beginning of the function to make them visible and clear.error function, that is used to throw an IllegalStateException. It is often used to handle a situation that we do not expect to ever take place, like an unexpected value type.// error implementation from Kotlin stdlib public inline fun error(message: Any): Nothing = throw IllegalStateException(message.toString()) // example usage fun handleMessage(message: Message) = when(message) { is TextMessage -> showTest(message.text) is ImageMessage -> showImage(message.image) else -> error("Unknown message type") }
- Make them more visible.
- Protect your application stability.
- Protect your code correctness.
- Smart cast variables.
requireblock - a universal way to specify expectations for arguments.checkblock - a universal way to specify expectations for states.-
errorfunction - a universal way to signal that application reached an unexpected state.
- The Elvis operator with
returnorthrow.
!!, however, it is sometimes useful when we are sure that a variable is not null, but the compiler cannot infer it. One feature that helps us avoid !! is lateinit property initialization.[^maxOf]: In the Kotlin Standard Library, this function is called
maxOf, but it accepts any number of arguments.