Dealing with null in Dart

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. (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' #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. (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: Understanding null safety from the Official Dart documentation; Sound null safety from the Official Dart Documentation; Null Safety and Variables from the Official Dart Documentation; Why nullable types? from the Official Dart Blog; The road to Dart 3: A fully sound, null safe language from the Official Dart Blog; Announcing Dart null safety beta on the Official Dart Blog; Null class API documentation; Null class implementation source code at Github; Native NULL Assertions from Dart SDK on Github; Null safety in Dart from leancode; Dart Lesson 10: Null Safety (Part 1) — Why is Null Safety Needed? by @jige2025; Dart Lesson 11: Null Safety (Part 2) — Safe Operators Explained by @jige2025; Type Promotion and Nullability in Dart by Alexander Obregon on Substack; Understanding Null Safety in Dart: Why It Matters and How to Use It by Ugama Benedicta Kelechi on Medium; Dart: How to Write Shorter Null, False, Empty String, and Zero Checks from CodeStudy.net; Mastering Dart's Null Safety: From ? to ! and Everything In Between by @hiteshm_devapp; Master Null Safety in Dart: A Step-by-Step by Abhay Kumar Bhumihar on Medium. STATIC TYPE CHECKER TOOLS FOR DART by Snigdha Mokkapati Type Unsoundness in Practice: An Empirical Study of Dart by Gianluca Mezzetti, Anders Møller and Fabio Strocco Type Soundness in the Dart Programming Language by Fabio Strocco Have fun! And Hack well! Cover Image by aa aaa on Unsplash

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.