3. Null in Others

Finally, we will consider nil in Swift [1] and null in Kotlin [2], which are the more recent languages, comparing them with Java and C#. We will also briefly introduce std::optional in C++.

Swift 5

Swift's nil is safe. First, local variables are forced to be initialized at declaration time. So you don't have to worry about uninitialized variables.

Then, like Java's Optional<T>, Swift has the Optional<T> type [3] and its instance is an immutable object. However, unlike Java, we can describe it with the notation T?.

T? is only a syntax sugar representing Optional<T>, and we may consider it similar to java.util.Optional<T> that wraps a value of reference types in Java, or System.Nullable<T> that wraps a value of value types in C#.

There are some ways to access the wrapped value.

Unconditional unwrapping

Applying the forced unwrap operator (postfix ! to an expression) to an expression of type Optional<T> unwraps forcibly the value of type T. However, if the value does not exist, a runtime error occurs.

The ! postfix operator in Swift is the notation for the operation corresponding to the get() method of java.util.Optional<T> in Java and the Value property of System.Nullable<T> in C#.

Optional chaining

Apply the optional chaining operator (postfix ? to an expression) to an expression of type Optional<T> gives access to the method, property, or subscript on an instance of the wrapped type T. If no value exists, there is no access and the expression results in nil. If the type of the return value of the method, of the property, or of the subscript access is U, that of expression is U?.

It is defined that methods whose return value is of type Void returns (), that is, an empty tuple†1. Therefore, invoking a method whose return value is of type Void with the optional chaining operator results in the expression of type Void?, i.e., ()?. Also, since an expression that sets a value for a property is equivalent to an expression that invokes a setter method of type Void, the type of the expression that sets a value for a property with the optional chaining operator is also ()?.

†1 Void is a type alias of ().

Therefore, the result of access with the optional chaining operator can be compared to nil to see if the access was performed. The following code is illustrated in the official documentation:

// printNumberOfRooms() is a method whose return value is of type 'Void'.
if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")
}

// Sets a value to the 'address' property.
if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}

Nil-coalescing operator

The nil-coalescing operator (??) is an operator that provides a default value when no value is present in an expression of type Optional<T>. For example, when expr1 is of an option type, expr1 ?? expr2 is an expression representing the value of expr1 if it exists, or the value of expr2 otherwise.

Optional binding

Swift provides us the following flow-control notations to take the wrapped value in an object of Optional<T> into another variable when the value is present:

  • if let
  • guard let
  • switch

You can use if let like is pattern matching in C#:

if let value = maybeNil {
    // If 'maybeNil' wraps any value: 'value' has the value in this scope
    ...
} else {
    // Otherwise
    ...
}

However, when there are two or more values of the option type, the nesting of the code tends to be deeper†2 as follows:

if let value1 = maybeNil1 {
    if let value2 = maybeNil2 {
        if let value3 = maybeNil3 {
            // Followed by the code that uses 'value1', 'value2', and 'value3'
            ...

†2 Incidentally, with is pattern matching in C#, it tends as well. But since the scope of variables in C# is different from Swift, you can invert the condition of if and use it like the guard let in Swift. However, in not actively introducing such use cases, Microsoft probably doesn't like the idea of Early Exit [4]. The description of the is pattern matching in C# [5] states:

The samples in this topic use the recommended construct where a pattern match is expression definitely assigns the match variable in the true branch of the if statement. You could reverse the logic by saying if (!(shape is Square s)) and the variable s would be definitely assigned only in the false branch. While this is valid C#, it is not recommended because it is more confusing to follow the logic.

The guard let solves this problem. It allows us to write the code that has a structure to do return when one of the required values doesn't exist, as follows:

guard let value1 = maybeNil1 else {
    return
}
guard let value2 = maybeNil2 else {
    return
}
guard let value3 = maybeNil3 else {
    return
}
// Followed by the code that uses 'value1', 'value2', and 'value3'
...

Of course, flow controls other than return are possible, for example, break or continue can be available if it is in a loop. Note that it is not necessary to use the guard in combination with the let. The constants and variables assigned in the conditional expression of the guard statement are available until the scope containing the guard statement closes, so they are also useful for Early Exit with guards other than nil checks.

Finally, there is the binding with switch, specifying the constant name and ? after case let as follows:

func printValue(_ maybeString: String?) {
    switch maybeString {
    case let value?:
        // When 'maybeString' has a value, it is assigned to 'value'
        print("value: \(value)")
        break
    default:
        // When 'maybeString' is nil
        print("no value")
        break
    }
}

printValue("foo")
printValue(nil)

The output results in:

value: foo
no value

Run

Also, you can use a tuple for switch to check several values as once:

func printValues(_ maybeInt1: Int?, _ maybeInt2: Int?) {
    switch (maybeInt1, maybeInt2) {
    case let (value1?, value2?):
        print("values: (\(value1), \(value2))")
        break
    default:
        print("one of the values is nil.")
        break
    }
}

printValues(2, 3)
printValues(4, nil)
printValues(nil, nil)

The output results in:

values: (2, 3)
one of the values is nil.
one of the values is nil.

Run

Consistency of option types in the standard library

The option type in Swift is much better than Optional<T> in Java and nullable types in C# because it is built in from the beginning as a basic feature of the standard library. For example, the return value of the subscript access (subscript) of Dictionary<K, V> is of type V?, and that of first of Array<E> is of type E?.

For better understanding, I would like to mention the compactMap of the Sequence protocol. compactMap takes a closure that converts the element in the sequence to a value of type T? as an argument and generates the new sequence containing the elements of type T. That is, compactMap converts the elements in the sequence to the objects of the option type with the closure of its argument, removes those that do not wrap a value, and then unwraps and retrieves the values from them, thus generating the new sequence containing only the elements of type T. This operation includes both the removal of nil and the conversion of type T? to T. What is important is that the static analysis makes it clear that the converted sequence does not contain any nil. In contrast, even if you use filter to the sequence containing the elements of type T? to remove the nil elements, the compiler assumes that the generated sequence has the elements of type T?.

Let's illustrate this with LINQ in C#. The following code intends to take a list containing the elements of the reference types, but the elements may be null, and to generate and return the list that does not contain null:

public static IEnumerable<T> WhereNonNull<T>(this IEnumerable<T?> list)
    where T : class
{
    var newList = list.Where(e => e is {});
    ...

Thus, you can use Where to the list containing null, to create the newList that does not contain null. At first glance, it seems that you get the newList of type IEnumerable<T> and reach the goal. However, the actual type of newList is IEnumerable<T?>. That is, both the original list and the generated newList have the same element type T?. Therefore, the static analysis assumes that newList may contain null.

Note that, in C#, you can use the OfType method to achieve both removing null and converting from type T? to T:

public static IEnumerable<T> WhereNonNull<T>(this IEnumerable<T?> list)
    where T : class
{
    var newList = list.OfType<T>();
    ...

Run

You can use this to imitate the Swift's compactMap in C#, as follows:

public static IEnumerable<U> CompactReferenceMap<T, U>(
    this IEnumerable<T> list,
    Func<T, U?> transform)
    where U : class
{
    return list.Select(e => transform(e))
        .OfType<U>();
}

Run

Also, you do so in Java†3, as follows:

private static <T, U> List<U> compactMap(
        List<T> list,
        Function<T, Optional<U>> transform) {
    return list.stream()
        .map(e -> transform.apply(e))
        .flatMap(o -> o.stream())
        .collect(Collectors.toList());
}

Run

†3 We used the stream() method of the Optional class, which has been available since Java 9. The API reference has also a similar description.

Kotlin 1.3

Kotlin's null is as safe as Swift's nil. See the official reference, which describes null safety [6], for the full story.

The major difference from Swift is that T? is not an option type but a nullable type. The nullable type is fake, as is the nullable reference type in C# 8. In other words, the compiler realizes the nullable types with static analysis. JetBrains, which invented Kotlin, is also the company that develops the Java IDE — IntelliJ IDEA. As explained in the Java 11 part, the compiler of IntelliJ IDEA can use the @NotNull/@Nullable annotation as a hint, to verify whether the null check is appropriate with data flow analysis. So it's no surprise that they also used that technology for Kotlin and its compiler.

The primitive types and nullable types

However, you should take care about the values of primitive types. For example, as described in the official documentation, the value of type Int? is a boxed Int object. This is the same as Java, where @Nullable Integer is possible but @Nullable int is not. The boxed primitive values preserve the equality of values, but may not preserve the identity of objects, as follows:

val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
// Prints 'true'
println(boxedA == anotherBoxedA)
// Prints 'false'
println(boxedA === anotherBoxedA)

Run

The operators for the nullable types

Although you might feel deja vu, the following operators are available for the nullable types:

  • .?(safe call operator)
  • ?:(Elvis operator)
  • !!(not-null assertion operator)

Each has the same meaning as the .?, ??, and ! postfix operators in C#/Swift, respectively. You can refer to the official reference for more details, so I only mention some interesting points.

You can apply the .? operator to l-value as well as in Swift. Also, you can combine the .? operator with the let function†4 to do something similar to ifPresent(Consumer) and map(Function) of the Optional<T> class in Java, as follows:

val item: String? = ...
item?.let { println(it) }
val length = item?.let { it.length }

†4 More precisely, you can combine not only let but also the scope functions described in the official documentation, such as run, apply, also.

The right term of the ?: operator can be return or throw instead of an expression. The following code is the illustration quoted from [6]:

fun foo(node: Node): String? {
    val parent = node.getParent() ?: return null
    val name = node.getName() ?: throw IllegalArgumentException("name expected")
    ...

The nullable types and the collections

The Array class and the Iterable interface have the mapNotNull method, which corresponds to compactMap in Swift. They also provide the filterNotNull method to get only the elements that have a value from the collection whose elements are of nullable types.

Like Swift, the existence of such APIs is the advantage of the languages that have nullable types from the beginning.

The platform types

Interoperability with Java is one of the key features of Kotlin. However, from Kotlin's viewpoint, all the reference types from Java are nullable, so using thoughtlessly the Java APIs poses a threat to the null safety. In other words, if you call the Java APIs and treat all the return values as of nullable types, there should be full of errors, which causes you to add !! earnestly. In the meantime, the errors that you have to fix are buried and then the null safety collapses.

The designers of Kotlin were smart, so they provided special types called platform types [7] to handle values coming from Java. However, it is no silver bullet, but the types that simply turn off data flow analysis for null at compile time, that is, the types to which the !! operator is implicitly applied. This makes it just a matter that if you neglect the null check for instances from Java, the NPE will be thrown at run time. The following code is an illustration quoted from [6]:

// 'list' is of the non-nullable type (the result of constructors)
val list = ArrayList<String>()
list.add("Item")

// 'size' is of the non-nullable type (the primitive type)
val size = list.size

// 'item' is of the platform types (the ordinary Java object)
val item = list[0]

// There is no error at compile time, but the next line throws an exception
// if 'item' is 'null' at run time.
item.substring(1)

// No problem.
val nullable: String? = item

// There is no error at compile time, but it may fail soon at run time.
val notNull: String = item

Thus, not all values from Java will be of platform types. Such as constructor results and primitive type values that are obvious to be non-null will be of non-nullable types. You should immediately assign the value of platform types to a variable of nullable types, or of non-nullable types if you are convinced that it is non-null.

Kotlin has no notation for writing the platform types. However, there is only the notation for the compiler to describe the type in errors, etc. The compiler displays the platform type that means “T or T?” as T!. Here is an example from [6]:

  • (Mutable)Collection<T>!
  • Array<(out) T>!

The former represents null or a reference to “A mutable or immutable Java collection which elements are of type T,” the latter null or a reference to “A Java array whose elements are of type T or the subtypes of T.”

Note that the Kotlin compiler understands the annotations around null described in the Java part, so if you annotate the Java API that Kotlin references with @NotNull and @Nullable, you can prevent Java objects from being of platform types.

C++17

C++ introduced the nullptr keyword in C++11 and the std::optional class in C++17. The std::optional class is intended to solve the problems similar to what Java's Optional tries to solve.

In C/C++, an array is not an object. Therefore, it is not possible to mimic “returning an array of length 0 or 1 instead of null” as described in the Java part. Of course, you can do something similar to that with such as std::vector instead of arrays. But many of the motivations for using C++ are not to tolerate such overhead.

The C++ standard prohibits the implementation of std::optional from dynamically allocating memory (for storing the value)†5. The standardization committee has already taken measures against those who reject the adoption of the option types for performance reasons.

†5 I'll supplement an explanation just in case you misunderstand. An object of type std::optional<T> allocates memory in advance for storing the value of type T when it is instantiated. This means that there is no dynamic memory allocation when the object of type std::optional<T> stores a value of type T. Typical implementations allocate the byte array of length sizeof(T) and then store a value there with a placement new. And this shows us that, unlike Java and Swift, it is impossible to store a value of the derived types of T.

Creating objects

Here is an example of a declaration of objects of type std::optional<int> with a value:

std::optional<int> v1(123);
std::optional<int> v2 {{123}};
std::optional<int> v3 = 123;
auto v4 = std::optional<int>(123);
auto v5 = std::make_optional<int>(123);

Compile

Everything will have the same result. Similarly, here is an example of a declaration with no value:

std::optional<int> n1;
std::optional<int> n2 {};
std::optional<int> n3 = std::nullopt;
auto n4 = std::optional<int>();

Compile

Likewise, they all have the same result.

Checking whether a value is present and accessing the value

You can do fewer things with C++17's std::optional than option types and nullable types in other languages. The std::optional does not have the operations that accept lambda expressions, such as ifPresent(Consumer) and map(Function) methods of Java's Optional. Before that, the C++ standard library currently lacks APIs for list comprehension. So, having such a thing only in std::optional will not dramatically improve usability.

In the future, features allowing you to do what you can do in other languages may be available since there is the following proposal:

p0798R3 Monadic operations for std::optional

You can obtain the presence or absence of the value of std::optional with the has_value() member function, which returns a value of type bool. However, you don't have to use this function. Since std::optional has operator bool (implicit type conversion to type bool), you can specify the instance directly to a conditional expression of such as if:

std::optional<int> maybeInt = ...;
if (maybeInt) {
    // 'maybeInt.has_value()' returns 'true', i.e., the value is present:
    ...
} else {
    // 'maybeInt.has_value()' returns 'false', i.e., the value is absent:
    ...
}

To get the value, you can use the operator * or value() member functions. These results differ only if there is no value. In this case, the former is undefined behavior and the latter throws the exception std::bad_optional_access.

If there is a value, you can also use operator -> to access the members of the value:

std::optional<std::string> maybeString = ...;
if (maybeString) {
    // The next statement is equivalent to:
    //
    // auto &s = *maybeString;
    // auto size = s.size();
    auto size = maybeString->size();
    ...
}

Run

However, as well as operator *, it is undefined behavior if there is no value.

The value_or(T) member function returns the value if it is present, otherwise the value of the argument:

std::optional<std::string> maybeString = ...;
auto s = maybeString.value_or(defaultValue);

Lazy initialization

Interestingly, unlike Optional in Java, Nullable in C#, and Optional in Swift, instances of std::optional in C++ are not immutable objects. It is possible to change the state of whether a value is present or not and to change the value to another value keeping that state. You can use the change from with no value to with a value to realize lazy initialization (See: Late Evaluation in #7 Immutable Object and Lazy initialization in #12 Java Memory Model ).

To change the state of an object of type std::optional<T> from with no value to with a value, or to change the value to something else, you can, for example, call the emplace(...) member function, assign an object of type T with operator =, or assign another object of type std::optional that has a value. On the other hand, to change the state from with a value to with no value, you can, for example, call the reset() member function, or assign std::nullopt with operator =.

As an example of the lazy initialization, consider a class Calculator that accepts a string representing a calculation formula with its constructor and returns the value, which is the result of evaluating the formula, with getValue(). We assume the following use cases:

int main() {
    Calculator c("(8 * 7 + 6) / 4");
    std::cout << c.getValue() << std::endl;
}

Here is an example implementation of a class Calculator that defers the evaluation of a calculation formula until the first call to getValue():

class Calculator {
public:
    Calculator(std::string expr) : expr(expr) {
    }

    int getValue() {
        if (!value) {
            value.emplace(evalExpr());
        }
        return *value;
    }

private:
    std::string expr;
    std::optional<int> value;

    // Evaluates 'expr' and returns the value.
    int evalExpr() {
        return ...
    }
};

Run

Let's consider a more practical example. Suppose that we want a class Foo to have a member bar of type Bar, but the Bar class does not have a default constructor. Moreover, it is assumed that we can not initialize bar with the constructor of Foo and that we have to defer initialization of bar.

Before C++17, this can be resolved with std::unique_ptr as follows:

class Foo {
public:
    Foo() {
        ...
    }

    void initialize() {
        bar = std::make_unique<Bar>(...);
    }

private:
    std::unique_ptr<Bar> bar;
};

However, using std::optional allows lazy initialization without dynamic memory allocation:

class Foo {
public:
    Foo() {
        ...
    }

    void initialize() {
        bar.emplace(...);
    }

private:
    std::optional<Bar> bar;
};

From undefined behavior to throwing an exception

The main attraction of using std::optional in C++ is throwing the exception std::bad_optional_access. Let's suppose, for example, there is an API that does not use std::optional but returns nullptr. If the return value is nullptr and you access it without a null check, the undefined behavior occurs. However, if the API returns std::optional, accessing the return value with value without checking whether it has a value will result in just throwing an exception. This difference is significant.

However, because there are historical assets that return nullptr or NULL, the use of std::optional only for the newly created APIs should be a drop in the bucket. (It could change over time, but ...)

The gsl::not_null class in the C++ Core Guidelines

Although not part of the standard library, the gsl::not_null class in the C++ Core Guidelines [9] is available as a gimmick to handle non-null pointers. The following section deals with Microsoft's implementation of Guidelines Support Library (GSL) [10].

Unlike smart pointers, the authors design the gsl::not_null<T> class so that T can be the type of any pointer (that is, U *) or any smart pointer†6. For example, you can write a function that takes a non-null argument of “const char *” type as follows:

std::size_t length(gsl::not_null<const char *> s)
{
    return std::strlen(s);
}

Similarly, you can write gsl::not_null<std::shared_ptr<U>> for a non-null argument of std::shared_ptr<U> type.

†6 You can use the std::shared_ptr or std::unique_ptr in the standard library for smart pointer types, but not std::weak_ptr. That is because T must be the class that the instance is comparable with nullptr (i.e., the expression v != nullptr can be evaluated against the value v of type T), but std::weak_ptr does not satisfy that requirement. There are also other requirements, such as the ability to apply a unary operator * to T. Of course, you can specify the appropriate class as T if it meets these requirements.

There is the constructor of gsl::not_null<T> that takes an argument of type T, so you can call the function length as follows:

auto n = length("hello");

Run

However, a call that takes a null pointer constant as an argument results in a compile error:

auto n = length(nullptr);

Compile

At runtime, the constructor of gsl::not_null<T> with a value of type T calls std::terminate() to exit if the value is equal to null. It ensures that the pointer is non-null after the constructor returns if T is U * (or something like std::shared_ptr<U>).

Be careful when you designate your custom class to T that can have a non-null value at the construction of the gsl::not_null<T> object but a null value when dereferencing it. When you try to dereference the gsl::not_null<T> object, it compares the instance of type T with nullptr (that is, performs a null check) before dereferencing the one with a unary operator * of T. Then, if the instance is equal to nullptr, it exits by calling std::terminate(), as it did at construction.

Once you construct the gsl::not_null<T> object, you can get the value of type T (a pointer to U) from it with its member function get(), and like a smart pointer, you can access it with unary operators * and ->. Also, since implicit type conversion from gsl::not_null<T> to T (i.e., non-null to nullable) is allowed, you may specify the value of type gsl::not_null<T> where type T is expected, as the call of std::strlen() in the above example.

The gsl::not_null often makes it unnecessary to check arguments for nulls at the beginning of functions as follows:

void foo(const void *p)
{
    assert(p != nullptr);
    ...
}

However, pointer operations and subscript access with gsl::not_null<T> objects, such as ++s and s[1], are not allowed, so you must assign them to other variables of type T if necessary. The idea seems to be that pointers should point to single objects (like references).

References

  1. Wikipedia, Swift (programming language)

  2. Wikipedia, Kotlin (programming language)

  3. Apple, Swift Standard Library, Numbers and Basic Values, Optional

  4. Apple, The Swift Programming Language, Language Guide, Control Flow

  5. Microsoft, Pattern Matching (C# guide)

  6. Kotlin Foundation, Kotlin Programming Language, Language Guide, Null Safety

  7. Kotlin Foundation, Kotlin Programming Language, Language Guide, Calling Java code from Kotlin

  8. Wikipedia, List comprehension

  9. Bjarne Stroustrup, Herb Sutter. C++ Core Guidelines

  10. Microsoft, GSL: Guidelines Support Library