Uses of Nothing
On the surface, Nothing
may seem useless. And, in day-to-day development, it is unlikely that you will use Nothing
directly… but it is very likely that you are using Nothing
without realizing it.
As a Return Type
A function can return Nothing
as a type. This might seem impossible, since there are no instances of Nothing
. From a compiler standpoint, though, a function returning Nothing
means the function is required to never return. The primary scenario of this is if the function is guaranteed to throw an exception.
That may sound strange… until you consider TODO()
.
TODO()
is declared to return Nothing
:
public inline fun TODO(reason: String): Nothing =
throw NotImplementedError("An operation is not implemented: $reason")
TODO()
is marked as being an inline
function, so the compiler will “bake” the exception right into wherever the TODO()
appears. However, the Nothing
return type indicates to the compiler that since TODO()
can never return, there is no sense in worrying about anything else in the function after that point, including the missing return
.
For example, suppose we created our own similar function, called NOTDONE()
:
public inline fun NOTDONE(reason: String) {
throw NotImplementedError("$reason")
}
fun heyThisIsNotDoneYet(): Int {
NOTDONE("wut")
}
fun main() {
heyThisIsNotDoneYet()
}
This fails compilation with A 'return' expression required in a function with a block body ('{...}')
for our heyThisIsNotDoneYet()
function. Even though NOTDONE()
is inline
, and so heyThisIsNotDoneYet()
will always throw an exception, the compiler is not quite smart enough to figure that out on its own. Adding Nothing
as the return type to NOTDONE()
clears that up:
public inline fun NOTDONE(reason: String): Nothing {
throw NotImplementedError("$reason")
}
fun heyThisIsNotDoneYet(): Int {
NOTDONE("wut")
}
fun main() {
heyThisIsNotDoneYet()
}
On the Right Side of Elvis
TODO()
is not the only function that returns Nothing
. error()
does as well:
public inline fun error(message: Any): Nothing = throw IllegalStateException(message.toString())
error()
forms nice shorthand for throwing an exception based on a failed null
check using the Elvis operator:
fun main() {
val something: String? = "foo"
val somethingNotNull = something ?: error("hey, that was null!")
println(somethingNotNull)
}
The reason why this compiles is that Nothing
is a sub-type of all types, including String
. Hence, from a type-safety standpoint:
- The left side of the Elvis operator is a
String
, because we know that it is notnull
- The right side of the Elvis operator is
Nothing
- The compiler finds the common supertype of those, which is
String
, and considers the expression’s overall type to beString
For Covariant Generics
The out
keyword signifies covariance in a generic type: we can accept the type or any sub-type. List
, for example, uses out
:
interface List<out E> : Collection<E>
As a result, we can use a List<Nothing>
in place of any other typed List
: List<String>
, List<Axolotl>
, etc.
The same holds true for any covariant generic type.
For Generic Singletons
That previous section might seem esoteric. After all, if there are no instances of Nothing
, we certainly cannot have a List
of such instances. However, just because a List<Nothing>
is impossible does not mean that a List<Nothing>
has no uses.
For example, suppose we need an empty list of something. One way to do that is to use listOf()
with no contents:
val things: List<String> = listOf()
An alternative is to use emptyList()
:
val things: List<String> = emptyList()
Those look nearly identical, but they actually have significantly different implementations. listOf()
will instantiate an empty List
, allocating a bit of memory along the way. emptyList()
does not… because emptyList()
returns a singleton:
public fun <T> emptyList(): List<T> = EmptyList
…and EmptyList
is a List<Nothing>
:
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
private const val serialVersionUID: Long = -7390468764508069838L
override fun equals(other: Any?): Boolean = other is List<*> && other.isEmpty()
override fun hashCode(): Int = 1
override fun toString(): String = "[]"
override val size: Int get() = 0
override fun isEmpty(): Boolean = true
override fun contains(element: Nothing): Boolean = false
override fun containsAll(elements: Collection<Nothing>): Boolean = elements.isEmpty()
override fun get(index: Int): Nothing =
throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
override fun indexOf(element: Nothing): Int = -1
override fun lastIndexOf(element: Nothing): Int = -1
override fun iterator(): Iterator<Nothing> = EmptyIterator
override fun listIterator(): ListIterator<Nothing> = EmptyIterator
override fun listIterator(index: Int): ListIterator<Nothing> {
if (index != 0) throw IndexOutOfBoundsException("Index: $index")
return EmptyIterator
}
override fun subList(fromIndex: Int, toIndex: Int): List<Nothing> {
if (fromIndex == 0 && toIndex == 0) return this
throw IndexOutOfBoundsException("fromIndex: $fromIndex, toIndex: $toIndex")
}
private fun readResolve(): Any = EmptyList
}
From a practical standpoint, both listOf()
and emptyList()
fill the role of an empty list, but because emptyList()
returns a singleton, emptyList()
does not allocate memory.
From a compilation standpoint, emptyList()
can be applied to any type, because Nothing
is a sub-type of any type.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.