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 representingOptional<T>
, and we may consider it similar tojava.util.Optional<T>
that wraps a value of reference types in Java, orSystem.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 theget()
method ofjava.util.Optional<T>
in Java and theValue
property ofSystem.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 ofif
and use it like theguard 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 theis
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 thetrue
branch of theif
statement. You could reverse the logic by sayingif (!(shape is Square s))
and the variables
would be definitely assigned only in thefalse
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
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.
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>();
...
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>();
}
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());
}
†3 We used the
stream()
method of theOptional
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)
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 asrun
,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 typeT
when it is instantiated. This means that there is no dynamic memory allocation when the object of typestd::optional<T>
stores a value of typeT
. Typical implementations allocate the byte array of lengthsizeof(T)
and then store a value there with a placementnew
. And this shows us that, unlike Java and Swift, it is impossible to store a value of the derived types ofT
.
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);
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>();
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:
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();
...
}
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 theemplace(...)
member function, assign an object of typeT
withoperator =
, or assign another object of typestd::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 thereset()
member function, or assignstd::nullopt
withoperator =
.
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 ...
}
};
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
orstd::unique_ptr
in the standard library for smart pointer types, but notstd::weak_ptr
. That is becauseT
must be the class that the instance is comparable withnullptr
(i.e., the expressionv != nullptr
can be evaluated against the valuev
of typeT
), butstd::weak_ptr
does not satisfy that requirement. There are also other requirements, such as the ability to apply a unary operator*
toT
. 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");
However, a call that takes a null pointer constant as an argument results in a compile error:
auto n = length(nullptr);
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 thegsl::not_null<T>
object but a null value when dereferencing it. When you try to dereference thegsl::not_null<T>
object, it compares the instance of typeT
withnullptr
(that is, performs a null check) before dereferencing the one with a unary operator*
ofT
. Then, if the instance is equal tonullptr
, it exits by callingstd::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
-
Apple, Swift Standard Library, Numbers and Basic Values, Optional
-
Apple, The Swift Programming Language, Language Guide, Control Flow
-
Kotlin Foundation, Kotlin Programming Language, Language Guide, Null Safety
-
Kotlin Foundation, Kotlin Programming Language, Language Guide, Calling Java code from Kotlin