The Haxe compiler offers opt-in compile-time checking for nullable values. It attempts to catch various possible issues with nullable values.
To enable the checker for a particular class, field, or expression, annotate it with the :nullSafety
metadata. Null safety can be enabled for a whole package using the --macro nullSafety("some.package")
initialization macro.
There are four levels of null safety strictness:
Off
: Turn off null safety checks. Useful to selectively disable null safety for particular fields or expression.Loose
(Default): Within an if (<expr> != null)
condition, <expr>
is considered safe even if it could be modified after the check.Strict
: Full-scale null safety checking for a single-threaded environment.StrictThreaded
: Full-scale null safety checking for a multi-threaded environment.Enabling null safety by default uses the loose strictness level. This can be configured by providing an argument in the metadata:
@:nullSafety(Off) @:nullSafety(Loose) @:nullSafety(Strict) @:nullSafety(StrictThreaded)
Strict
and StrictThreaded
differ in handling of sequential field access.
In a multi-threaded application sequential access to the same object field may not yeld the same result. That means a null check for a field does not provide any guarantees:
@:nullSafety(StrictThreaded) function demo1(o:{field:Null<String>}) { if (o.field != null) { // Error: o.field could have been changed to `null` // by another thread after the check trace(o.field.length); } } @:nullSafety(Strict) function demo1(o:{field:Null<String>}) { if (o.field != null) { trace(o.field.length); // Ok } }
For the package-level case, null safety strictness can be configured using the optional second argument:
--macro nullSafety("some.package", Off) --macro nullSafety("some.package", Loose) --macro nullSafety("some.package", Strict) --macro nullSafety("some.package", StrictThreaded)
Null<T>
(assignments, return statements, array access, etc.).@:nullSafety class Main { static function getNullableStr():Null<String> { return null; } public static function main() { function fn(s:String) {} var nullable:Null<String> = getNullableStr(); // all of the following lines would cause a compilation error: // var str:String = null; // var str:String = nullable; // fn(nullable); } }
==
and !=
) is not allowed.Null<>
then it should have an initial value or it should be initialized in the constructor (for instance fields).var nullables:Array<Null<String>> = ['hello', null, 'world']; // Array<Null<String>> cannot be assigned to Array<String>: //var a:Array<String> = nullables;
null
are considered safe inside of a scope covered with that null-check:var nullable:Null<String> = getSomeStr(); //var s:String = nullable; // Compilation error if (nullable != null) { s = nullable; //OK } //s = nullable; // Compilation error s = (nullable == null ? 'hello' : nullable); // OK switch (nullable) { case null: case _: s = nullable; // OK }
function doStuff(a:Null<String>) { if(a == null) { return; } // From here `a` is safe, because function execution // will continue only if `a` is not null: var s:String = a; // OK }
null
, but Haxe types them without Null<>
.var a:Array<String> = ["hello"]; $type(a[100]); // String trace(a[100]); // null var s:String = a[100]; // Safety does not complain here, because `a[100]` is not `Null<String>`, but just `String`
null
values. Null safety cannot protect against this.var a:Array<String> = ["hello"]; a[2] = "world"; trace(a); // ["hello", null, "world"] var s:String = a[1]; // Cannot check this trace(s); //null
null
values will come into your code from third-party code or even from the standard library.null
. You can use helper methods instead:using Main.NullTools; class NullTools { public static function sure<T>(value:Null<T>):T { if (value == null) { throw "null pointer in .sure() call"; } return @:nullSafety(Off) (value:T); } public static function or<T>(value:Null<T>, defaultValue:T):T { if (value == null) { return defaultValue; } return @:nullSafety(Off) (value:T); } } class Main { static var nullable:Null<String>; public static function main() { var str:String; if (nullable != null) { str = nullable; // Compilation error } str = nullable.sure(); str = nullable.or('hello'); } }
var a:Null<String> = getSomeStr(); var fn = function () { if (a != null) { var s:String = a; // Compilation error } }
Unless the closure is executed immediately:
var a:Null<String> = getSomeStr(); [1, 2, 3].map(function (i) { if (a != null) { return i * a.length; // OK } else { return i; } });
var nullable:Null<String> = getSomeNullableStr(); var str:String; if (nullable != null) { str = nullable; // OK doStuff(function () nullable = getSomeNullableStr()); if (nullable != null) { str = nullable; // Compilation error } }