11 Nullability¶
You know that game where you try to find the item that doesn’t belong in a list? Here’s one for you:
horse, camel, pig, cow, sheep, goat
Which one doesn’t belong?
It’s the third one, of course! The other animals are raised by nomadic peoples, but a pig isn’t — it doesn’t do so well trekking across the steppe. About now you’re probably muttering to yourself why your answer was just as good — like, a sheep is the only animal with wool, or something similar. If you got an answer that works, good job. Here’s another one:
196, 144, 169, 182, 121
Did you get it? The answer is one hundred and eighty-two. All the other numbers are squares of integers.
One more:
3, null, 1, 7, 4, 5
And the answer is . . . null
! All of the other items in the list are integers, but null
isn’t an integer.
What? Was that too easy?
Null Overview¶
As out of place as null
looks in that list of integers, many computer languages actually include it. In the past Dart did, too, but starting with version 2.12, Dart decided to take null
out of the list and only put it back if you allow Dart to do so. This feature is called sound null safety.
Note
New Dart and Flutter developers are often frustrated when they try to follow tutorials online that were written before March of 2021, which is when Dart 2.12 came out. Modern Dart complains with lots of errors about that old Dart code. Sometimes the solution is as easy as adding ?
after the type name. Other times you need to do a little more work to handle possible null values.
What Null Means¶
Null means “no value” or “absence of a value”. It’s quite useful to have such a concept. Imagine not having null at all. Say you ask a user for their postal code so that you can save it as an integer in your program:
int postalCode = 12345;
Everything will go fine until you get a user who doesn’t have a postal code. Your program requires some value, though, so what do you give it? Maybe 0
or -1
?
int postalCode = -1;
Choosing a number like -1
, though, is somewhat arbitrary. You have to define it yourself to mean “no value” and then tell other people that’s what it means.
// Hey everybody, -1 means that the user
// doesn't have a postal code. Don't forget!
int postalCode = -1;
On the other hand, if you can have a dedicated value called null
, which everyone already understands to mean “no value”, then you don’t need to add comments explaining what it means.
int postalCode = null;
It’s obvious here that there’s no postal code. In versions of Dart prior to 2.12, that line of code worked just fine. However, now it’s no longer allowed. You get the following error:
A value of type 'Null' can't be assigned to a variable of type 'int'.
What’s wrong? Null is a useful concept to have! Why not allow it, Dart?
The Problem With Null¶
As useful as null is for indicating the absence of a value, developers do have a problem with it. The problem is that they tend to forget that it exists. And when developers forget about null, they don’t handle it in their code. Those nulls are like little ticking time bombs ready to explode.
To see that in action, open the file with your main
function and replace the contents of the file with the following:
void main() {
print(isPositive(3)); // true
print(isPositive(-1)); // false
}
bool isPositive(dynamic anInteger) {
return !anInteger.isNegative;
}
Using dynamic
turns off Dart’s null safety checks and will allow you to observe what it’s like to get a surprise null
value.
Run that code and you’ll get a result of true
and false
as expected. The isPositive
method works fine as long as you give it integers. But what if you give it null
?
Add the following line to the bottom of the main
function:
print(isPositive(null));
Run that and your program will crash with the following error:
NoSuchMethodError: The getter 'isNegative' was called on null.
You learned previously that null means “no value”, which is true, semantically. However, the Dart keyword null
actually is a value in the sense that it’s an object. That is, the object null
is the sole instance of the Null
class. Because the Null
class doesn’t have a method called isNegative
, you get a NoSuchMethodError
when you try to call null.isNegative
.
Now replace dynamic
with int
in your isPositive
function:
bool isPositive(int anInteger) {
return !anInteger.isNegative;
}
This turns Dart’s null checking back on. All of a sudden you now have an error where you tried to call your function with null
:
With sound null safety, you can’t assign a null
value to an int
even if you wanted to. Eliminating the possibility of being surprised by null
prevents a whole class of errors.
Delete that line with the null error. Problem solved.
But wait? Isn’t null useful? What about a missing postal code?
Yes, null is useful and Dart has a solution.
Nullable vs. Non-Nullable Types¶
Dart separates its types into nullable and non-nullable. Nullable types end with a question mark (?
) while non-nullable types do not.
Non-Nullable Types¶
Dart types are non-nullable by default. That means they’re guaranteed to never contain the value null
, which is the essence of the meaning of sound in the phrase “sound null safety”. These types are easy to recognize because, unlike nullable types, they don’t have a question mark at the end.
Here are some example values that non-nullable types could contain:
int
:3
,1
,7
,4
,5
double
:3.14159265
,0.001
,100.5
bool
:true
,false
String
:'a'
,'hello'
,'Would you like fries with that?'
User
:ray
,vicki
,anonymous
These are all acceptable ways to set the values:
int myInt = 1;
double myDouble = 3.14159265;
bool myBool = true;
String myString = 'Hello, Dart!';
User myUser = User(id: 42, name: 'Ray');
If you replaced any of the values on the right with null
, Dart would give you a compile-time error.
Nullable Types¶
A nullable type can contain the null
value in addition to its own data type. You can easily tell the type is nullable because it ends with a question mark (?
), which is like saying, “Maybe you’ve got the data you want or maybe you’ve got null
. That’s the question.” Here are some example values that nullable types could contain:
int?
:3
,null
,1
,7
,4
,5
double?
:3.14159265
,0.001
,100.5
,null
bool?
:true
,false
,null
String?
:'a'
,'hello'
,'Would you like fries with that?'
,null
User?
:ray
,vicki
,anonymous
,null
That means you can set any of them to null
:
int? myInt = null;
double? myDouble = null;
bool? myBool = null;
String? myString = null;
User? myUser = null;
The question mark at the end of String?
isn’t an operator acting on the String
type. Rather, String?
is a whole new type separate from String
. String?
means that the variable can either contain a string or it can be null
. It’s a union of the String
and Null
types.
Every non-nullable type in Dart has a corresponding nullable type: int
and int?
, bool
and bool?
, User
and User?
, Object
and Object?
. By choosing the type, you get to choose when you want to allow null values and when you don’t.
Note
The non-nullable type is a subtype of its nullable form. For example, String
is a subtype of String?
since String?
can be a String
.
For any nullable variable in Dart, if you don’t initialize it with a value, it’ll be given the default value of null
.
Create three variables of different nullable types:
int? age;
double? height;
String? message;
Then print them:
print(age);
print(height);
print(message);
You’ll see null
for each value.
Exercises¶
- Create a
String?
variable calledprofession
, but don’t give it a value. Then you’ll haveprofession
null
. Get it? Professional? :] - Give
profession
a value of “basketball player”. - Write the following line and then hover your cursor over the variable name. What type does Dart infer
iLove
to be?String
orString?
?
const iLove = 'Dart';
Handling Nullable Types¶
The big problem with the old nullable types in the past was how easy it was to forget to add code to handle null
values. That’s no longer true. Dart now makes it impossible to forget because you really can’t do much at all with a nullable value until you’ve dealt with the possibility of null
.
Try out this example:
String? name;
print(name.length);
Dart doesn’t let you run that code, so there isn’t even an opportunity to get a runtime NoSuchMethodError
like before. Instead, Dart gives you a compile-time error:
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.
Compile-time errors are your friends because they’re easy to fix. In the next few sections, you’ll see how to use the many tools Dart has to deal with null values.
Type Promotion¶
The Dart analyzer is the tool that tells you what the compile-time errors and warnings are. It’s smart enough to tell in a wide range of situations if a nullable variable is guaranteed to contain a non-null value or not.
Take the last example, but this time assign name
a string literal:
String? name;
name = 'Ray';
print(name.length);
Even though the type is still nullable, Dart can see that name
can’t possibly be null
because you assigned it a non-null value right before you used it. There’s no need for you to explicitly “unwrap” name
to get at its String
value. Dart does this for you automatically. This is known as type promotion. Dart promotes the nullable and largely unusable String?
type to a non-nullable String
with no extra work from you! Your code stays clean and beautiful. Take some time right now to send the Dart team a thank-you letter.
Flow Analysis¶
Type promotion works for more than just the trivial example above. Dart uses sophisticated flow analysis to check every possible route the code could take. As long as none of the routes come up with the possibility of null
, it’s promotion time!
Take the following slightly less trivial example:
bool isPositive(int? anInteger) {
if (anInteger == null) {
return false;
}
return !anInteger.isNegative;
}
In this case, you can see that by the time you get to the anInteger.isNegative
line, anInteger
can’t possibly be null
because you’ve already checked for that. Dart’s flow analysis could also see that, so Dart promoted anInteger
to its non-nullable form, that is, to int
instead of int?
.
Even if you had a much longer and nested if-else
chain, Dart’s flow analysis would still be able to determine whether to promote a nullable type or not.
Null-Aware Operators¶
In addition to flow analysis, Dart also gives you a whole set of tools called null-aware operators that can help you handle potentially null values. Here they are in brief:
??
: If-null operator.??=
: Null-aware assignment operator.?.
: Null-aware access operator.?.
: Null-aware method invocation operator.!
: Null assertion operator.?..
: Null-aware cascade operator.?[]
: Null-aware index operator....?
: Null-aware spread operator.
The following sections describe in more detail how most of these operators work. The last two, however, require a knowledge of collections, so you’ll have to wait until Chapter 12, “Lists”, to learn about them.
If-Null Operator (??
)¶
One convenient way to handle null
values is to use the ??
double question mark, also known as the if-null operator. This operator says, “If the value on the left is null
, then use the value on the right.” It’s an easy way to provide a default value for when a variable is empty.
Take a look at the following example:
String? message;
final text = message ?? 'Error';
Here are a couple of points to note:
- Since
message
isnull
,??
will settext
equal to the right-hand value:'Error'
. Ifmessage
hadn’t beennull
, it would have retained its value. - Using
??
ensures thattext
can never benull
, thus Dart infers the variable type oftext
to beString
and notString?
.
Print text
to confirm that Dart assigned it the 'Error'
string rather than null
.
Using the ??
operator in the example above is equivalent to the following:
String text;
if (message == null) {
text = 'Error';
} else {
text = message;
}
That’s six lines of code instead of one when you use the ??
operator. You know which one to choose.
Null-Aware Assignment Operator (??=
)¶
In the example above, you had two variables: message
and text
. However, another common situation is when you have a single variable that you want to update if its value is null
.
For example, say you have an optional font-size setting in your app:
double? fontSize;
When it’s time to apply the font size to the text, your first choice is to go with the user-selected size. If they haven’t chosen one, then you’ll fall back on a default size of 20.0
. One way to achieve that is by using the if-null operator like so:
fontSize = fontSize ?? 20.0;
However, there’s an even more compact way to do it. In the same way that the following two forms are equivalent,
x = x + 1;
x += 1;
there’s also a null-aware assignment operator (??=
) to simplify if-null statements that have a single variable:
fontSize ??= 20.0;
If fontSize
is null
, then it will be assigned 20.0
, but otherwise, it retains its value. The ??=
operator combines the null check with the assignment.
Both ??
and ??=
are useful for initializing variables when you want to guarantee a non-null value.
Null-Aware Access Operator (?.
)¶
Earlier with anInteger.isNegative
, you saw that trying to access the isNegative
property when anInteger
was null
caused a NoSuchMethodError
. There’s also an operator for null safety when accessing object members. The null-aware access operator (?.
) returns null
if the left-hand side is null
. Otherwise, it returns the property on the right-hand side.
Look at the following example:
int? age;
print(age?.isNegative);
Since age
is null
, the ?.
operator prevents that code from crashing. Instead, it just returns null
for the whole expression inside the print
statement. Run that and you’ll see the following:
null
Internally, a property is just a getter method on an object, so the ?.
operator works the same way to call methods as it does to access properties.
Therefore, another name for ?.
is the null-aware method invocation operator. As you can see, invoking the toDouble()
method works the same way as accessing the isNegative
property:
print(age?.toDouble());
Run that and it’ll again print “null” without an error.
The ?.
operator is useful if you want to only perform an action when the value is non-null. This allows you to gracefully proceed without crashing the app.
Null Assertion Operator (!
)¶
Sometimes Dart isn’t sure whether a nullable variable is null
or not, but you know it’s not. Dart is smart and all, but machines don’t rule the world yet.
So if you’re absolutely sure that a variable isn’t null
, you can turn it into a non-nullable type by using the null assertion operator (!
), or sometimes more generally referred to as the bang operator.
String nonNullableString = myNullableString!;
Note the !
at the end of myNullableString
.
Note
In Chapter 5, “Control Flow”, you learned about the not-operator, which is also an exclamation mark. To differentiate the not-operator from the null assertion operator, you can also refer to the not-operator as the prefix !
operator because it goes before an expression. By the same reasoning, you can refer to the null assertion operator as the postfix !
operator since it goes after an expression.
Here’s an example to see the assertion operator at work. In your project, add the following function that returns a nullable Boolean:
bool? isBeautiful(String? item) {
if (item == 'flower') {
return true;
} else if (item == 'garbage') {
return false;
}
return null;
}
Now in main
, write this line:
bool flowerIsBeautiful = isBeautiful('flower');
You’ll see this error:
A value of type 'bool?' can't be assigned to a variable of type bool
The isBeautiful
function returned a nullable type of bool?
, but you’re trying to assign it to flowerIsBeautiful
, which has a non-nullable type of bool
. The types are different, so you can’t do that. However, you know that 'flower'
is beautiful; the function won’t return null
. So you can use the null assertion operator to tell Dart that.
Add the postfix !
operator to the end of the function call:
bool flowerIsBeautiful = isBeautiful('flower')!;
Now there are no more errors.
Alternatively, since bool
is a subtype of bool?
, you could also cast bool?
down using the as
keyword that you learned about in Chapter 3, “Types & Operations”.
bool flowerIsBeautiful = isBeautiful('flower') as bool;
This is equivalent to using the assertion operator. The advantage of !
is that it’s shorter.
Beware, though. Using the assertion operator, or casting down to a non-nullable type, will crash your app with a runtime error if the value actually does turn out to be null
, so don’t use it unless you can guarantee that the variable isn’t null
.
Here’s an alternative that won’t ever crash the app:
bool flowerIsBeautiful = isBeautiful('flower') ?? true;
You’re leaving the decision up to the function, but giving it a default value by using the ??
operator.
Think of the !
assertion operator as a dangerous option and one to be used sparingly. By using it, you’re telling Dart that you want to opt out of null safety. This is similar to using dynamic
to tell Dart that you want to opt out of type safety.
Note
You’ll see a common and valid use of the null assertion operator in the section below titled No Promotion for Non-Local Variables.
Null-Aware Cascade Operator (?..
)¶
In Chapter 8, “Classes”, you learned about the ..
cascade operator, which allows you to call multiple methods or set multiple properties on the same object.
Give a class like this:
class User {
String? name;
int? id;
}
If you know the object isn’t nullable, you can use the cascade operator like so:
User user = User()
..name = 'Ray'
..id = 42;
However, if your object is nullable, like in the following example:
User? user;
Then you can use the ?..
null-aware cascade operator):
user
?..name = 'Ray'
..id = 42;
You only need to use ?..
for the first item in the chain. If user
is null
, then the chain will be short-circuited, or terminated, without calling the other items in the cascade chain.
This is similar for the null-aware access operator (?.
) as well. Look at this example:
String? lengthString = user?.name?.length.toString();
Since user
might be null
, it needs the ?.
operator to access name
. Since name
also might be null
, it needs the ?.
operator to access length
. However, as long as name
isn’t null
, length
will never be null
, so you only use the .
dot operator to call toString
. If either user
or name
is null
, then the entire chain is immediately short-circuited and lengthString
is assigned null
.
Initializing Non-Nullable Fields¶
When you create an object from a class, Dart requires you to initialize any non-nullable member variables before you use them.
Say you have a User
class like this:
class User {
String name;
}
Since name
is String
and not String?
, you must initialize it somehow. If you recall what you learned in Chapter 8, “Classes”, there are a few different ways to do that.
Using Initializers¶
One way to initialize a property is to use an initializer value:
class User {
String name = 'anonymous';
}
In this example, the value is 'anonymous'
, so Dart knows that name
will always get a non-null value when an object is created from this class.
Using Initializing Formals¶
Another way to initialize a property is to use an initializing formal, that is, by using this
in front of the field name:
class User {
User(this.name);
String name;
}
Having this.name
as a required parameter ensures that name
will have a non-null value.
Using an Initializer List¶
You can also use an initializer list to set a field variable:
class User {
User(String name)
: _name = name;
String _name;
}
The private _name
field is guaranteed to get a value when the constructor is called.
Using Default Parameter Values¶
Optional parameters default to null
if you don’t set them, so for non-nullable types, that means you must provide a default value.
You can set a default value for positional parameters like so:
class User {
User([this.name = 'anonymous']);
String name;
}
Or like this for named parameters:
class User {
User({this.name = 'anonymous'});
String name;
}
Now even when creating an object without any parameters, name
will still at least have a default value.
Required Named Parameters¶
As you learned in Chapter 7, “Functions”, if you want to make a named parameter required, use the required
keyword.
class User {
User({required this.name});
String name;
}
Since name
is required, there’s no need to provide a default value.
Nullable Instance Variables¶
All of the methods above guaranteed that the class field will be initialized, and not only initialized, but initialized with a non-null value. Since the field is non-nullable, it’s not even possible to make the following mistake:
final user = User(name: null);
Dart won’t let you do that. You’ll get the following compile-time error:
The argument type 'Null' can't be assigned to the parameter type 'String'
Of course, if you want the property to be nullable, then you can use a nullable type, and then there’s no need to initialize the value.
class User {
User({this.name});
String? name;
}
String?
makes name
nullable. Now it’s your responsibility to handle any null
values it may contain.
No Promotion for Non-Local Variables¶
One topic that people often get confused about is the lack of type promotion for nullable instance variables.
As you recall from earlier, Dart promotes nullable variables in a method to their non-nullable counterpart if Dart’s flow analysis can guarantee the variable will never be null:
bool isLong(String? text) {
if (text == null) {
return false;
}
return text.length > 100;
}
In this example, the local variable text
is guaranteed to be non-null if the line with text.length
is ever reached, so Dart promotes text
from String?
to String
.
However, take a look at this modified example:
class TextWidget {
String? text;
bool isLong() {
if (text == null) {
return false;
}
return text.length > 100; // error
}
}
The line with text.length
now gives an error:
The property 'length' can't be unconditionally accessed because the receiver can be 'null'.
Why is that? You just checked for null
after all.
The reason is that the Dart compiler can’t guarantee that other methods or subclasses won’t change the value of a non-local variable before it’s used.
Since Dart has gone the path of sound null safety, this guarantee is essential before type promotion can happen.
You do have options, however. One is to use the !
operator:
bool isLong() {
if (text == null) {
return false;
}
return text!.length > 100;
}
Even if the compiler doesn’t know that text
isn’t null, you know it’s not, so you apply that knowledge with text!
.
Another option is to shadow the non-local variable with a local one:
class TextWidget {
String? text;
bool isLong() {
final text = this.text; // shadowing
if (text == null) {
return false;
}
return text.length > 100;
}
}
The local variable text
shadows the instance variable this.text
and the compiler is happy.
The Late Keyword¶
Sometimes you want to use a non-nullable type, but you can’t initialize it in any of the ways you learned above.
Here’s an example:
class User {
User(this.name);
final String name;
final int _secretNumber = _calculateSecret();
int _calculateSecret() {
return name.length + 42;
}
}
You have this non-nullable field named _secretNumber
. You want to initialize it based on the return value from a complex algorithm in the _calculateSecret
instance method. You have a problem, though, because Dart doesn’t let you access instance methods during initialization.
The instance member '_calculateSecret' can't be accessed in an initializer.
To solve this problem, you can use the late
keyword. Add late
to the start of the line initializing _secretNumber
:
late final int _secretNumber = _calculateSecret();
Dart accepts it now, and there are no more errors.
Using late
means that Dart doesn’t initialize the variable right away. It only initializes it when you access it the first time. This is also known as lazy initialization. It’s like procrastination for variables.
It’s also common to use late
to initialize a field variable in the constructor body. Here’s an alternate version of the example above:
class User {
User(this.name) {
_secretNumber = _calculateSecret();
}
late final int _secretNumber;
// ...
}
Initializing a final variable in the constructor body wouldn’t have been allowed if it weren’t marked as late
.
Dangers of Being Late¶
The example above was for initializing a final variable, but you can also use late
with non-final variables. You have to be careful with this, though:
class User {
late String name;
}
Dart doesn’t complain at you, because using late
means that you’re promising Dart that you’ll initialize the field before it’s ever used. This moves checking from compile-time to runtime.
Now add the following code to main
and run it:
final user = User();
print(user.name);
You broke your word and never initialized name
before you used it. Dart is disappointed with you, and complains accordingly:
LateInitializationError: Field 'name' has not been initialized.
For this reason, it’s somewhat dangerous to use late
when you’re not initializing it either in the constructor body or in the same line that you declare it.
Like with the null assertion operator (!
), using late
sacrifices the assurances of sound null safety and puts the responsibility of handling null
into your hands. If you mess up, that’s on you.
Benefits of Being Lazy¶
Who knew that it pays to be lazy sometimes? Dart knows this, though, and uses it to great advantage.
There are times when it might take some heavy calculations to initialize a variable. If you never end up using the variable, then all that initialization work was a waste. Since lazyinitialization is never done until you actually use the variable, though, this kind of work will never be wasted.
Top-level and static variables have always been lazy in Dart. As you learned above, the late
keyword makes other variables lazy, too. That means even if your variable is nullable, you can still use late
to get the benefit of making it lazy.
Here’s what that would look like:
class SomeClass {
late String? value = doHeavyCalculation();
String? doHeavyCalculation() {
// do heavy calculation
}
}
The method doHeavyCalculation
is only run after you access value
the first time. And if you never access it, you never do the work.
Well, that wraps up this chapter. Sound null safety makes Dart a powerful language and is a relatively rare feature among the world’s computer programming languages. Aren’t you glad you chose to learn Dart?
Challenges¶
Before moving on, here are some challenges to test your knowledge of nullability. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.
Challenge 1: Naming Customs¶
People around the world have different customs for giving names to children. It would be difficult to create a data class to accurately represent them all, but try it like this:
- Create a class called
Name
withgivenName
andsurname
properties. - Some people write their surname last and some write it first. Add a Boolean property called
surnameIsFirst
to keep track of this. - Not everyone in the world has a surname.
- Add a
toString
method that prints the full name.
Key Points¶
- Null means “no value.”
- A common cause of errors for programming languages in general comes from not properly handling
null
. - Dart 2.12 introduced sound null safety to the language.
- Sound null safety distinguishes nullable and non-nullable types.
- A non-nullable type is guaranteed to never be
null
. - Null-aware operators help developers to gracefully handle
null
.
?? if-null operator
??= null-aware assignment operator
?. null-aware access operator
?. null-aware method invocation operator
! null assertion operator
?.. null-aware cascade operator
?[] null-aware index operator
...? null-aware spread operator
- The
late
keyword allows you to delay initializing a field in a class. - Using
late
also makes initialization lazy, so a variable’s value won’t be calculated until you access the variable for the first time. late
and!
opt out of sound null safety, so use them sparingly.
Where to Go From Here?¶
In the beginning, Dart didn’t support null safety. It’s an evolving and ever-improving language. Since development and discussions about new features all happen out in the open, you can watch and even participate. Go to dart.dev/community to learn more.