How is Dart "sound null-safety" different from Kotlin null safety?

2.7k views Asked by At

This Dart official video states that Dart's so-called "sound null safety" is better than Kotlin's null safety design, because it can optimise the code based on whether a variable is declared nullable, and other languages (I assume this refers to languages including Kotlin) have to do runtime checks to ensure null safety.

So, what extra optimization does Dart do?

How does it interoperate with legacy codebases that are not null-safe (written before null safety) while ensuring null safety?

2

There are 2 answers

6
creativecreatorormaybenot On

Dart sound null safety

So, what extra optimization does Dart do?

The benefit of sound null safety in Dart is that the compiler can make use of a non-nullable type, which can elimate null checks. Therefore, the compiler will generate fewer instructions (which results in smaller binaries and faster runtime).

Example

Take the following function:

int getAge(Animal a) {
  return a.age;
}

These are the instructions the compiler generated before sound null safety:

compiled getAge function without null safety

As you can see, there are explicit instructions for checking null in the compiled code.

And this is what the same function looks like compiled with sound null safety:

compiled getAge function with null safety

Now, these additional instructions are no longer needed.

Note that the actual instructions generated starting with Dart 2.12 for the example function are the following (there were further optimizations):

compiled getAge function Dart 2.12 sound null safety

See Dart and the performance benefits of sound types by Vijay Menon (Engineering Lead, Dart) for reference.

Interoperability with legacy Dart code bases

How does it interoperate with legacy codebases that are not null-safe (written before null safety) while ensuring null safety?

It does not.

Well, that is not the whole truth. If you want to use Dart >=2.12.0 with any codebase that was written before Dart 2.12 (and with that before null safety), you cannot make use of sound null safety. You can, however, interoperate with these legacy codebases by passing a compiler flag that disables sound null safety. That would be --no-sound-null-safety (see my previous answer for more details).

This means that all benefits of sound null safety are lost when interacting with legacy codebases. This is also why the Dart team encourages all package authors to migrate their code to null safety.

Comparison to Kotlin

Kotlin simply does not have the additional compiler optimizations that Dart achieves with unboxed values thanks to sound null safety.

Keep in mind that Kotlin always allows interoperability with Java, which does not have any concept of null safety. I would imagine that this is a reason why Kotlin will never be able to have sound null safety in the same way that Dart code that interoperates with legacy codebases does not. That is as long as Kotlin code is compiled for the JVM with Java interoperability.

NNBD

If we are not concerned about the compiled code but only about the developer experience, Kotlin and Dart handle null safety identically. That is both languages are non-nullable by default (NNBD).

This means that when writing code in Dart 2.12+ or in Kotlin, all types are assumed to be non-nullable unless you explicitly mark them as nullable.
The only way to get a null pointer exception is by programmer error in both languages, i.e. using the bang operator ! in Dart and double bang operator in Kotlin !!, i.e. a not-null assertion by the developer.

Note that when using null assertions, additional runtime checks have to be added to the compiled code to preserve soundness in Dart. These checks always exists for Kotlin code compiled for the JVM as it is not sound to begin with.

This can also happen when interoperating with Java code when using Kotlin or interoperating with legacy code when using Dart.

There are some more edge cases, see Null safety in Kotlin and Understanding null safety in Dart for reference.

9
mezoni On

So, what extra optimization does Dart do?

The most basic kind of optimization is that when performing calculations on numeric types, compiler can treat them (internally) as primitive types of non-reference types (unboxed values).

Why is that?

Because they cannot be null and, therefore, it is not necessary to use them as data of referenced types (boxed values).

Why is that?

Because null is represented in Dart as a null constant reference.
If there is no need to refer to this constant, then why not use value types instead of reference type? At least in the generated code, which can be optimized already at compile time.

All this thanks to the so-called "strong mode".
The strong mode in conjunction with non-nullable types allows you to optimize the code already at the compilation stage, which is very important for modes such as AOT, which do not allow code to be optimized at runtime, because it is in the RE (read and execute) mode.

How does it interoperate with legacy codebases that null-safety is not supported while ensuring null safety?

It seems to me that you should ask this as a separate question.