1. Introduction

Kotlin brought into its type system one significant change compared to Java: nullability. Where in Java we need to use Optional explicitly, in Kotlin, all values either explicitly cannot be null, or else we have to null-check any access to them.

In Java, null values often dictate a rather awkward coding style, where null checks obscure the program’s business logic. In this tutorial, we’ll try to offer some approaches to treating nulls expressively with Kotlin.

2. On Idiomatic Kotlin Code

To talk about the idiomatic way to treat null values, we have to define what’s idiomatic for Kotlin. Kotlin is a language closely related to Java, and yet it tries to handle things in a much better way.

On the class member level, Kotlin offers to follow the functional paradigm: using immutable structures, favoring pure functions, and even having functions as first-class citizens. Moreover, within the functional paradigm, any program is nothing more than a set of filters or transformations over a pipeline of data.

That is why Kotlin, Scala, and other functional languages so favor the fluent style:

fun fluent(a: A): String = a
  .toB()
  .toC()
  .toString()

Transformations follow one another step by step, and the reader’s attention is focused on the actions, not on the data structures.

Another idea is to make a program able to handle all possible valid inputs. This concept is present in the OOP paradigm as polymorphism, whereas in functional programming, its close relative is called pattern-matching:

fun patterMatching(a: Any): C = when(a) {
    is A -> TODO("Do something")
    is B -> TODO("Do something else")
    is C -> TODO("Do more things still")
    else -> TODO("Do default")
}

To be sure, there is more to functional programming than these two concepts, but they’ll be enough to cover handling nulls.

3. Elvis Operator and Returning a Value

First of all, almost no encounter with a nullable value happens without a safe call operator:

nested?.value

We can chain these safe calls:

nested?.value?.subvalue?.subsubvalue

They work exactly like a Maybe monad: If at any step we encounter a null, this is what we return as a result. Otherwise, we return subsubvalue.

A close relative of a safe call is the Elvis operator:

fun elvisStacking(flag: String?) =
  flag
    ?.let { transform(it) }
    ?.let { transformAgain(it) }
    ?: "erised"

Basically, it folds our nullable type, allowing us to offer an “else” to the safe call “if not null”. There could be more than one data transformation stage, starting with a safe call and ending with an Elvis operator, to cover the possibility of some result in the chain being null after all.

Using both safe calls and the Elvis operator, we can build a pipeline of data transformations that process our non-null input correctly and return a proper response in case the input or the result of one of the transformations is null.

4. Elvis Operator and Returning Unit

The previous example works well if we expect to return something. However, it isn’t always the case. Suppose that, instead of the result, we want our function to have a side-effect. Then any lambda can be forced to return an empty value, Unit. Methods that would be specified in Java with a void return type are defined to return Unit in Kotlin:

fun elvisStackingWithUnitDefault(flag: String?): Unit =
  flag
    ?.let { transform(it); Unit }
    ?: println("erised")

That being said, relying on side effects is a dangerous architectural approach. Usually, it’s a good idea to write functions that return values. Such functions are easier to test and support.

Despite this approach also using a fluent style, it’s an example that defeats the original idea because we chain side effects and not transformations.

5. if Expression

Though Java has the ternary operator, it isn’t a good idea to use it everywhere instead of if… else… Sometimes, readability requires more explicit language constructs. Kotlin has our back there as well — there’s nothing that prevents us from writing an if-expression:

fun ifExpression(flag: String?) =
  if (flag.isNullOrBlank()) "erised" else transform(flag)

Unlike in Java, a Kotlin if-expression has a result and can be returned, but only if it’s a full expression, with both if and else branches.

This approach also works better than the safe call if the results we return from branches aren’t transformations of the value we test for nullability.

6. when Expression

Finally, nullable types can have more values of special interest to us, except null. Other languages have the switch-case operator, which allows us to go over multiple values, but these usually work on primitive types only, like numbers or strings. Kotlin’s when can match by type, as well as by value, and also works with non-primitive types:

data class Snitch(val content: Any)

fun whenExpression(flag: Any?) =
  when (flag) {
      null -> "erised"
      is Exception -> "expelliarmus"
      "socks" -> "Silente"
      "doe" -> "Piton"
      "family" -> "the boy"
      Snitch("the stone") -> "a hallow"
      else -> "babbano"
  }

Among many possible values we match against, there could be the null value as well. These powerful abilities of when make it a pattern-matching tool, which allows us to branch among many possible logical paths.

7. Conclusion

Kotlin provides multiple ways of handling nullability. Depending on the requirements, we may choose different means from the Kotlin pallette. The Elvis operator covers most of the cases where we need to do one thing in case our value is Some and another if it’s None.

In case there are many possibilities we need to cover, then when pattern-matching is a good answer. Sometimes it’s a good idea to be more explicit and use the full if… else… construct.

All the examples, as usual, are available over on GitHub.


» 下一篇: Kotlin中的SOLID原则