DEV Community

Cover image for Dealing with null in Dart
Mathieu Kerjouan
Mathieu Kerjouan

Posted on

Dealing with null in Dart

Into the void of silence, into the empty space of nothing, the joy of life is unfurled.

-- C. S. Lewis

Usually, a developer will focus on errors and exceptions to increase the stability of its application. It can be with defensive programming patterns, or like for Erlang, embracing errors and accepts them with the Let it crash philosophy (see also crash-only systems). In Dart... it was weird to see more information about null than errors management. In fact, one of the longest article on dart.dev seems to be dedicated to null safety.

How to deal with errors, exceptions and... null, in Dart? It will probably be the thing I will hate the most with Dart. Dealing with null seems to be really complex, at least, when no external packages are used.

What is null anyway?

The reserved word null evaluates to the null object.

   ⟨nullLiteral⟩ ::= null

The null object is the sole instance of the built-in class Null. Attempting to instantiate Null causes a compile-time error. It is a compile-time error for a
class to extend, mix in or implement Null. The Null class extends the Object class and declares no methods except those also declared by Object. The null object has primitive equality (10.2.3). The static type of null is the Null type
-- Dart Specification, section 17.4, page 103

So, null is an unique object only instantiated once during - probably - the initialization.

The treatment of null merits some discussion. Consider null + 2. This expression always causes an error. We could have chosen not to treat it as a constant expression (and in general, not to allow null as a subexpression of numeric or boolean constant expressions). There are two arguments for including it: First, it is constant so we can evaluate it at compile time. Second, it seems more useful to give the error stemming from the evaluation explicitly.
-- Dart Specification, page 101

Errors generate by nulls can be evaluated at compile time, this is helpful for static analysis.

Bootstrapping

For this article, a new project called n will be created. It will be used to play with null in Dart.

$ dart create n
Creating n using template console...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  bin/n.dart
  lib/n.dart
  test/n_test.dart

Running pub get...                     2.7s
  Resolving dependencies...
  Downloading packages...
  Changed 48 dependencies!

Created project n in n! In order to get started, run the following commands:

  cd n
  dart run

$ cd n
Enter fullscreen mode Exit fullscreen mode

Dissecting Null

The Null object got only two attributes, hashCode and runtimeType. The hashCode returns always 0 (zero) and the runtimeType returns the Type of the object, Null in this case. Only two methods are also public, noSuchMethod() and toString().

void main() {
  print(null.hashCode);
  print(null.runtimeType);
}
Enter fullscreen mode Exit fullscreen mode
0
Null
Enter fullscreen mode Exit fullscreen mode

Let have a quick look on the source code.

@pragma("vm:entry-point")
final class Null {
  factory Null._uninstantiable() {
    throw UnsupportedError('class Null cannot be instantiated');
  }

  external int get hashCode;

  String toString() => "null";
}
Enter fullscreen mode Exit fullscreen mode

The Null class is implemented in sdk/lib/core/null.dart as a final class. It uses a vm:entry-point pragma, confirming its presence during initialization. the _uninstantiable constructor is a bit odd to me. I don't know if it's convention or something specified somewhere, but because a factory is set, I would assume it is a way to protect against creating new instance during runtime. It could be nice to find some ways to call this method.

Operators

Let have a quick look on the Null operators. The first one to check will be the ! operator. It is only used if you are sure a variable will never be null, if the data is null, then it will raise an exception Null check operator used on a null value.

void t1(dynamic? v) {
  var v1 = v!;
  print("t1: ${v1}");
}

void main() {
  t1(1);
  t1(null);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built n:n.
t1: 1
Unhandled exception:
Null check operator used on a null value
#0      t1 (file:///home/user/tmp/dart/n/bin/n.dart:9:13)
#1      main (file:///home/user/tmp/dart/n/bin/n.dart:5:3)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#3      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Enter fullscreen mode Exit fullscreen mode

What to do if the value is null but a default value can be used instead? In this situation, the ?? (if-null) operator can be used.

An if-null expression evaluates an expression and if the result is the null object (17.4), evaluates another.

-- Dart Language Specification, Section 17.25, page 176

void t2(dynamic? v) {
  var v1 = v ?? "test";
  print(v1);
}

void main() {
  t2(1);
  t2(null);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built n:n.
t2: 1
t2: default value
Enter fullscreen mode Exit fullscreen mode
void t3(dynamic? v) {
  var v3 = null;
  v3 ??= v;
  print("t3: ${v3}");
}

void main() {
  t3(1);
  t3(null);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built n:n.
t3: 1
t3: null
Enter fullscreen mode Exit fullscreen mode
void t4(dynamic? v) {
  print(v?.toString());
}

void main() {
  t4(1);
  t4("test");
  t4(null);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built n:n.
1
test
null
Enter fullscreen mode Exit fullscreen mode
void t5(dynamic? v) {
  print([...? v]);
  print([... v]);
}

void main() {
  t5([1]);
  t5(["test"]);
  t5(null);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built n:n.
[1]
[1]
[test]
[test]
[]
Unhandled exception:
type 'Null' is not a subtype of type 'Iterable<dynamic>'
#0      t5 (file:///home/user/tmp/dart/n/bin/n.dart:31:14)
#1      main (file:///home/user/tmp/dart/n/bin/n.dart:6:3)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#3      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)
Enter fullscreen mode Exit fullscreen mode

It's All About Syntactic Sugar

I consume too much sugar. It’s a problem, I need to stop.

-- Kevin Jonas

Most of the previous examples are syntactic sugar around the conditional expression. This can be seen in the Figure 3 in the 17.20 section of the Dart Language specification.

A conditional expression evaluates one of two expressions based on a boolean condition

-- Dart Language Specification, Section 7.24, page 175

Let check these expression by first defining a new class R containing one attribute id and one method f. Nothing complex there, it's just to have a custom object.

class R {
  dynamic id = null;
  R(dynamic this.id);
  dynamic f(dynamic args) => args;
}
Enter fullscreen mode Exit fullscreen mode

The first syntactic sugar to test is r?.attribute. If r is null then it will return null, else r.id will be used.

(dynamic, dynamic, bool) sugar1(dynamic r) {
  var a = r?.id;
  var b = r?.id == null ? null : r.id;
  final bool res = a == b;
  final ret = (a,b,res);
  print("sugar1: $ret");
  return ret;
}
Enter fullscreen mode Exit fullscreen mode

The second one is the r?.attribute = e syntactic sugar. If r is null, nothing is set, but if it's not the case, the value e is set to the r.id attribute.

(dynamic, dynamic, dynamic) sugar2(dynamic r, dynamic e) {
  var a = r?.id = e;
  var b = r == null ? null : r.id = e;
  final bool res = a == b;
  final ret = (a,b,res);
  print("sugar2: $ret");
  return ret;
}
Enter fullscreen mode Exit fullscreen mode

The third one is about method calling r?.f(args). If r is null then it will return null, else the method f() is called.

(dynamic, dynamic, dynamic) sugar3(dynamic r, dynamic args) {
  var a = r?.f(args);
  var b = r == null ? null : r.f(args);
  final bool res = a == b;
  final ret = (a,b,res);
  print("sugar3: $ret");
  return ret;
}
Enter fullscreen mode Exit fullscreen mode

The fourth one is about List: r?[e]. If r is null then null is returned else, the index is used on the list r.

(dynamic, dynamic, dynamic) sugar4(dynamic r, dynamic e) {
  var a = r?[e];
  var b = r == null ? null : r[e];
  final bool res = a == b;
  final ret = (a,b,res);
  print("sugar4: $ret");
  return ret;
}
Enter fullscreen mode Exit fullscreen mode

The fifth one is still about list, this time, if r is null, nothing can be done on the list, but if r is not null, the index e1 will be used to set the value e2.

(dynamic, dynamic, dynamic) sugar5(dynamic r, dynamic e1, dynamic e2) {
  var a = r?[e1] = e2;
  var b = r == null ? null : r[e1] = e2;
  final bool res = a == b;
  final ret = (a,b,res);
  print("sugar5: $ret");
  return ret;
}
Enter fullscreen mode Exit fullscreen mode

Those functions can be added in our entry-point...

void main() {
  sugar1(R(1));
  sugar1(R(null));

  sugar2(R(1), 2);
  sugar2(R(null), 2);

  sugar3(R(1), 2);
  sugar3(R(null), 2);

  sugar4([1,2,3], 0);
  sugar4(null, 0);

  sugar5([1,2,3], 0, 1);
  sugar5(null, 0, 1);
}
Enter fullscreen mode Exit fullscreen mode

... And finally invoked.

$ run bin/n.dart
sugar1: (1, 1, true)
sugar1: (null, null, true)
sugar2: (2, 2, true)
sugar2: (2, 2, true)
sugar3: (2, 2, true)
sugar3: (2, 2, true)
sugar4: (1, 1, true)
sugar4: (null, null, true)
sugar5: (1, 1, true)
sugar5: (null, null, true)
Enter fullscreen mode Exit fullscreen mode

The results are similar. I know, this is a dummy test, something more accurate could have been done, but it was just to give the equivalent syntax one can use.

Conclusion

Nothing exists but atoms and the void.

-- Democritus

After a bit more than 1 month of coding with Dart, I found the null part exhausting. To me, this part of the language is close to the annoying error catching in Go... Perhaps a little bit better than that though. Adding more and more syntactic sugar to fix a problem like this one can lead to confusing syntax, and it seems to be the case. Using ?, ?? or ! in different part of the code makes everything more complex.

As usual, I am not the only one to write about this topic, and it would be a shape to not share all the resources I used to learn about null safety in Dart. Here the list:

Have fun! And Hack well!


Cover Image by aa aaa on Unsplash

Top comments (0)