Introduction
Null-safety has caused a series of problems for developers, especially in dart, where it was introduced as an optional feature and developers have had to slowly migrate to it usually not fully understanding it. This post is here to change that.
What exactly does null-safe mean?
To understand what null-safety is, first we need to understand what null is.
Null is a keyword in dart that means nothing, it is used to describe a variable with no value assigned. It should be used for variables that don't always have a value (for example a currentUser
variable could be null if no user is signed in), a nullable variable is different from a late variable in that a late variable starts with no value but once it gets a value it can never go back to having no value at all.
Late variables are also different in that they are never null, an uninitialized null variable will not be null, and trying to check if it is will throw an error
class MyClass {
late int myVar;
void myMethod() {
if (myVar != null) { // this if is reading an unasigned late variable
print(myVar);
}
}
}
void main() {
final myClass = MyClass();
myClass.myMethod();
}
The above will throw an error while attempting to read myVar
to see if it's null, you will also get a lint warning letting you know that myVar == null
will never be true.
On the other hand, a nullable variable can be used when planning to read the value before assigning it or when planning to assign null to it more than once.
Null-safety then, makes sure we only ever add null checks when necesary. Please take a look at the following example:
void main() {
int x = someFunction();
if (x != null) {
print(x);
} else {
print('x is null');
}
}
In the example above, the code if (x != null)
is redundant, because we declared x
to be a null-safe variable (int
, not int?
), meaning that either someFunction
will never return null, or we already got some sort of compilation error when assigning, either way, we don't need an if statement to know x
will never be null.
In case we want to work with a nullable variable, we can make it so by adding a ?
after the type declaration:
void main() {
int? x = someFunction(); // some function might return null
if (x != null) {
print(x);
} else {
print('x is null');
}
}
Above x
could be null assuming someFunction
can return null, so the if statement is no longer redundant.
Null-safety operators
Along with nullable variables, we have some useful operators, namely ?.
, ??
and !
, (the last one was added with null-safety, but the rest already existed).
The null aware operator
The first of the three operators I want us to take a look at is the ?.
operator, we'll call it the null-aware operator. The null aware operator can be used to access a nullable variable's instance fields, take a look at the following example:
String? myString = myFunction();
if (myString.isNotEmpty) {
print("My string wasn't empty");
}
The above code will not compile. myString
could be null, yet we are trying to get isNotEmpty
but if myString
is null, we will get an error, so we can't possibly compile this, this is where the ?.
operator comes into play, the ?.
operator will return null when used on a null value and it will read a property when used on a non-null value, take a look:
String? myString = myFunction();
if (myString?.isNotEmpty == true) {
print("My string wasn't empty and it wasn't null either!");
}
In the new example, we used ?.
, which means isNotEmpty
will be null if myString
is also null. With that in mind, I added a == true
after it to make sure that the if statement doesn't run if myString
is null
The if-null operator
The next operator is the ??
or if-null operator. It is quite a simple operator and as the name implies it can be used to provide a default value in case of null, take a look at this simplified example:
int? a = someNullableFunction();
int b = a ?? 0;
The above code can also be understood like this:
int? a = someNullableFunction();
int b;
if (a == null) {
b = 0;
} else {
b = a;
}
That is to say, the if-null operator will return the left operand if it isn't null and the right operand if the left is.
Of course, the operand can be linked together to form some confusing looking code:
int? a = someFunction();
int? b = someFunction();
int? c = someFunction();
int d = a ?? b ?? c ?? 0;
Above, d
will first try to assign itself to a
, but if a
is null, it will attempt to use b
, if b
is null it will use c
and if c
is null it will use 0.
The if-null operator can be used in conjunction with the null-aware operator for some pretty nice results, take a look at this slightly more realistic example:
@override
Widget build(BuildContext context) {
List<int>? myList = _generateList();
return ListView.builder(
itemCount: myList?.length ?? 0,
itemBuilder: (context, index) { ... },
);
}
Above, we don't know if our list of items will be null, but we can save ourselves a ternary by using null-awareness and if-null operators.
Last but not least, this operator can be used in conjunction with the =
operator to asign if null:
int? a = someFunction();
a ??= 0;
Above, a
will be 0 if it was null, but it will keep its old value otherwise, as with other assignment operators (like +=
), ??=
is just a synonym of:
int? a = someFunction();
a = a ?? 0;
Also, I do want to note that in this example we should just do this:
int a = someFunction() ?? 0;
which is more concise.
The null check operator
Finally, the !
or null check operator (not to be confused with the not operator, as that one is used before a boolean expression and the null check operator is used after a nullable expression).
It was introduced along with null-safety to dart and it has quite a simple function, it can be used to turn a nullable value into a non-null one:
int? a = myFunction();
int b = a!;
Using this operator we can easily assign a
to b
even if a
is nullable and b
is not.
Now I hear you asking what's the catch? What happens if a
is null? And I hear you!
The answer is very simple, you get a null-check error, meaning the program will crash if you use a !
operator on a null value, you should not use this operator on a value that might be null, it is meant to be used only if you know a nullable value isn't null, here is a very simple example:
class MyClass {
int? _value;
int? get value => _value;
void setValueIfNull(int val) {
_value ??= val;
}
}
void main() {
MyClass c = MyClass();
c.setValueIfNull(1);
int x = c.value!;
}
Above, we as the programmers know that MyClass.value
will not be null right after a call to MyClass.setValueIfNull()
, because said method will assign some value, however dart has no way of knowing this, so we can help dart out by using the !
operator on MyClass.value
when reading it. Here is another, simpler example:
int? a = someFunction();
if (a != null) {
int b = a!;
}
Now because we are inside an if statement that made sure a isn't null, we can use the !
operator freely, in reality, this code will work, but we will get a warning letting us know that using !
is not necesary because even dart is smart enough to know that a
is not null right after checking it, we will talk more on this topic in a second.
I think it is also worth mentioning that the null check operator can be used like this !.
to access a nullable variable's fields. Here is a slightly more realistic example:
@override
Widget build(BuildContext context) {
return FutureBuilder<List<int>>(
future: _getNumbers(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: shapshot.data!.length,
itemBuilder: ...
);
}
return CircularProgressIndicator();
}
);
}
As you can see, because we knew snapshot.data
wouldn't be null, we can use !
like this snapshot.data!.length
Type escalation
As I mentioned before if you put this code here:
int? a = someFunction();
if (a != null) {
int b = a!;
}
into the dart compiler, you will get a lint warning letting you know that there is no need to use !
because a
will never be null. This is what we refer to as type escalation, in this context, we have already made sure that a
will never be null, so for dart, the type of the variable a
has escalated from int?
to int
.
Keep in mind the real type of the variable is still int?
so we can still assign null, in this case, the type escalation will only work inside the if statement and last until we assign a
again.
Type escalation works outside of null-safety, but I believe it is especially useful when working with nullable values.
dynamic myVariable = someDynamicFunction();
if (myVariable is String) {
print(myVariable.substring(1)); // you should get intellisense here
}
It is also worth mentioning type escalation only works on local variables, the reason being it is hard to predict what getters will return, take a look at this example:
class MyClass {
void myMethod() {
if (myVar != null) {
int currentVar = myVar;
}
}
int? get myVar {
int myNum = someRandomNumberGenerator(min: 0, max: 100);
return myNum > 50 ? myNum : null;
}
}
As you can see above the global getter myVar
will return null
50% of the times it is called, meaning it won't necessarily be safe after making a check, so type escalation can't happen and the above code will give an error.
That's it! You are done reading this article, if you are interested in continuing to read about null-safety, I recommend you go to dart's official null-safety page which also contains tons of awesome resources to level up your null-safety skills!
Top comments (0)