How does null assertion work in dart and when can I use it? - dart

Can someone simply explain to me how null assertion (!) works and when to use it?

The ! operator can be used after any expression, e!.
That evaluates the expression e to value v, then checks whether v is the null value. If it is null, an error is thrown. If not, then e! also evaluates to v.
The static type of an expression e! is (basically) the static type of e with any trailing ?s remove. So, if e has type int?, the type of e! is int.
You should not use e! unless e can be null (the type of e is potentially nullable).
The ! operator is dynamically checked. It can throw at runtime, and there is no static check which can guarantee that it won't. It's like using a value with type dynamic in that all the responsibility of preventing it from throwing is on the author, the compiler can't help you, and you need good tests to ensure that it won't throw when it's not supposed to.
It's called an assertion because it should never throw in production code.
So, use e! when you know (for some reason not obvious to the compiler, perhaps because of some invariant guaranteeing that the value is not null while something else is true) that e is not null.
Example:
abstract class Box<T extends Object> {
bool hasValue;
T? get value;
}
...
Box<int> box = ...;
if (box.hasValue) {
var value = box.value!;
... use value ...
}
If you are repeatedly using ! on the same expression, do consider whether it's more efficient to read it into a local variable just once.
Also, if (like this Box example) the value being null is equivalent to the other test you just did, maybe just check that directly:
Box<int> box = ...;
var value = box.value;
if (value != null) {
... use value ...
}
This code, with an explicit != null check on a local variable, is statically guaranteed to not throw because the value is null.
The code using ! above relies on the author to maintain whichever invariant allowed them to write the !, and if something changes, the code might just start throwing at runtime. You can't tell whether it's safe just by looking at the code locally.
Use ! sparingly, just like the dynamic type and late declarations, because they're ways to side-step the compiler's static checking and ensure it that "this is going to be fine". That's a great feature when you need it, but it's a risk if you use it unnecessarily.

Related

is there a difference between implicit cast vs 'as' keyword in dart?

Is there any difference between using an implicit cast to cast in dart vs the 'as' keyword? Will they result in the same (or similar) runtime error if the type is not as expected?
For example:
dynamic foo = "blah";
String boo = foo; // is this
String boo2 = foo as String; // the same as this?
No. And yes.
TL;DR: Don't worry about the difference, just do what reads the best.
If your program is correct, and the casts will succeed, then there is unlikely to be any difference.
When inferring types, String boo = foo; will infer the type of foo with a context type of String. If the resulting static type of foo then turns out to be dynamic then it implies an implicit downcast from dynamic to `String.
For String boo = foo as String;, the static type of foo is inferred with no context type. No matter what the resulting static type is, it will be cast to String at run-time.
You can see a difference between these two if you have a more complicated expression than just the variable foo:
T first<T extends dynamic>(List<T> list) => list.first;
String boo = first([1]); // <- compile-time error
String boo2 = first([1]) as String;
With this example, you get a compile-time error in the boo line because the compiler knows that the list should be a List<String>. There is no error in the boo2 line because the list only needs to be a List<dynamic>, and whatever first returns is then dynamically cast to String.
A more contrived example would be:
T firstOrDefault<T extends dynamic>(List<T> list) {
if (list.isEmpty) {
// Invent some default values for known types.
if (null is T) return null as T;
if (0 is T) return 0 as T;
if (0.0 is T) return 0.0 as T;
if ("" is T) return "" as T;
if (false is T) return false as T;
throw UnsupportedError("No default value for the needed type");
}
return list.first;
}
String boo = firstOrDefault([]); // <- returns "", with null safety.
String boo2 = firstOrDefault([]) as String; // <- returns null, throws.
(Doing that kind of type-parameter specialization is not a recommended programming style. It's too fragile precisely because it can be affected in unpredictable ways by subtle changes to static types.).
Ignoring inference and static checking, there is not much difference at run-time. If foo is just a simple expression with static type dynamic, then the language requires downcast to String in both situations.
However, the Dart2JS web compiler can enable unsound optimizations which basically omit implicit downcasts entirely (as an "optimization" assume that they would have succeeded) and the go on with potentially type-unsound values flowing around.
For that reason, some people (mainly those coding for the web) may prefer to use implicit downcasts over explicit downcasts.
Dart with null safety only has implicit downcasts from dynamic.
You can always force an implicit downcast from any type by doing:
String boo3 = foo as dynamic;
The as dynamic is a free up-cast, it has no effect at run-time (it can't fail and the compiler knows that), so all it does is change the static type of the expression ... to something which introduces an implicit downcast, which the dart2js compiler will then (unsoundly) ignore as well.
(Use with caution, as with everything involving dynamic. Also, the analyzer might warn about an "unnecessary up-cast".)

Why flow analysis doesn't work when using bool?

void main() {
int? foo;
var isNonNull = foo != null;
if (isNonNull) foo.isEven; // Error
}
I already did a check on foo and stored its value in isNonNull variable. I could understand that warning if the scope wasn't local.
Note: I know I can use ! bang operator to resolve it but why flow analysis isn't working?
Dart type promotion is based on a check of a variable going a particular way dominating a later use of that variable.
So, if you do if (x != null) x.foo();, it detects that the check x != null being true means that the later x.foo() is valid. That only works because the compiler can also convince itself that the variable's value doesn't change between the check and the use.
If you introduce an extra boolean variable, like here, then the check is no longer performed inside the branch. That doesn't make it impossible to remember that the isNonNull boolean value being true means that foo is non-null, but it gets more complicated. The compiler now has to ensure that foo doesn't change and that isNotNull doesn't change.
The Dart compiler bails out instead, it doesn't track that level of complication. It only tracks promotion through one variable at a time.
You get the error because flow analysis can't know if isNonNull is ever going to get a new value. Take this example:
final random = Random();
void main() {
int? foo;
var isNonNull = foo != null;
isNonNull = random.nextBool();
if (isNonNull) foo.isEven;
}
So, its better to use
if (foo != null) foo.isEven;

An expression whose value can be 'null' must be null-checked before it can be dereferenced

I am using Dart with null-safity mode. I have porblem in next code.
List<JobModel> _jobList = [];
// ...
if(_jobList.isNotEmpty) {
for(var job in _jobList) {
if(job.cmd_args != null) {
numbersOfAlreadyActiveJobs.addAll(job.cmd_args.replaceAll('(', '').replaceAll(')','').split('=')[1].split(',').map(int.parse).toList());
}
}
I
am getting next error:
But why? I am doing check of job.cmd_args != null
Dart promotes local variables only, this goes both for types and null-ness.
When you do if (job.cmd_args != null) ..., it doesn't allow the compiler to assume that any later evaluation of job.cmd_args will also return a non-null value. It cannot know that without knowing the entire program (to ensure that nothing implements a JobModel where cmd_args might be a getter which does something different on the next call). Even if it knows that, we don't want local type promotion to depend on global information. That makes it too fragile and you can't depend on it. Someone adding a class somewhere else might make your code no longer promote, and therefore no longer compile.
So, your test is not enough.
What you can do is either say "I know what I'm doing" and add a ! to job.cmd_args!.replaceAll. That's a run-time cast to a non-nullable type, and it will throw if the value ends up actually being null on the second read (which it probably won't, but compilers aren't satisfied by "probably"s).
Alternatively, you can introduce a local variable, which can be promoted to non-null:
var args = job.cmd_args;
if (args != null) {
numberOfAlreadyActiveJobs.addAll(args.replaceAll(...));
}
Here you introduce a new local variable, args, and then you check whether that is null. If it isn't, then you can safely use it as non-null later because a local variable can't be affected by code anywhere else, so the compiler believes it really will have the same value when you use it later.

Null safety type promotion when assigning non-null value literal

In nullsafety.dartpad.dev if I write the following code:
void main() {
String? name = 'Bob';
print(name.length);
}
I get the following compile-time error:
An expression whose value can be 'null' must be null-checked before it can be dereferenced
And the following runtime error:
Property 'length' cannot be accessed on 'String?' because it is potentially null.
The Type promotion on null checks documentation says:
The language is also smarter about what kinds of expressions cause promotion. An explicit == null or != null of course works. But explicit casts using as, or assignments, or the postfix ! operator we’ll get to soon also cause promotion. The general goal is that if the code is dynamically correct and it’s reasonable to figure that out statically, the analysis should be clever enough to do so.
Question
There is no possible way name could be null in the code above. The documentation also says assignments should cause type promotion. Am I misunderstanding type promotion or is this a bug in DartPad?
Clarification
Since a couple of the answers are providing workaround solutions to the error messages, I should clarify that I'm not trying to solve the coding problem above. Rather, I'm saying that I think the code should work as it it. But it doesn't. Why not?
This answer is in response to the bounty that was added to the original question. The bounty reads:
Please explain how String? is different from String and how type
promotion works in Dart.
String? vs String
The type String? can contain a string or null. Here are some examples:
String? string1 = 'Hello world';
String? string2 = 'I ❤️ Dart';
String? string3 = '';
String? string4 = null;
The type String, on the other hand, can only contains strings (once null safety is a part of Dart, that is). It can't contain null. Here are some examples:
String string1 = 'Hello world';
String string2 = 'I ❤️ Dart';
String string3 = '';
If you try to do the following:
String string4 = null;
You'll get the compile-time error:
A value of type 'Null' can't be assigned to a variable of type 'String'.
The String type can't be null any more than it could be an int like 3 or a bool like true. This is what null safety is all about. If you have a variable whose type is String, you are guaranteed that the variable will never be null.
How type promotion works
If the compiler can logically determine that a nullable type (like String?) will never be null, then it converts (or promotes) the type to its non-nullable counterpart (like String).
Here is an example where this is true:
void printNameLength(String? name) {
if (name == null) {
return;
}
print(name.length);
}
Although the parameter name is nullable, if it actually is null then the function returns early. By the time you get to name.length, the compiler knows for certain that name cannot be null. So the compiler promotes name from String? to String. The expression name.length will never cause a crash.
A similar example is here:
String? name;
name = 'Bob';
print(name.length);
Although name is nullable here, too, the string literal 'Bob' is obviously non-null. This also causes name to be promoted to a non-nullable String.
The original question was regarding the following:
String? name = 'Bob';
print(name.length);
It seems that this should also promote name to a non-nullable String, but it didn't. As #lrn (a Google engineer) pointed out in the comments, though, this is a bug and when null safety comes out, this will also work like the previous example. That is, name will be promoted to a non-nullable String.
Further reading
Sound null safety
Type promotion on null checks
I understand what you are saying. Try this out.
In order for type promotion to work you must first confirm that the value is not null as the documentation says.
As you can see in the picture dart is able to do the type promotion or understand that name is not going to be null because it checks that on the if statement beforehand.
But if using it outside the if statement without checking if it is not null beforehand, dart knows it can be assigned null anytime again. That’s why it encourages always checking if it is null. Because any instatiated variable ( a variable with a value assigned) can be assigned null in the future.

Is None less evil than null?

In F# its a big deal that they do not have null values and do not want to support it. Still the programmer has to make cases for None similar to C# programmers having to check != null.
Is None really less evil than null?
The problem with null is that you have the possibility to use it almost everywhere, i.e. introduce invalid states where this is neither intended nor makes sense.
Having an 'a option is always an explicit thing. You state that an operation can either produce Some meaningful value or None, which the compiler can enforce to be checked and processed correctly.
By discouraging null in favor of an 'a option-type, you basically have the guarantee that any value in your program is somehow meaningful. If some code is designed to work with these values, you cannot simply pass invalid ones, and if there is a function of option-type, you will have to cover all possibilities.
Of course it is less evil!
If you don't check against None, then it most cases you'll have a type error in your application, meaning that it won't compile, therefore it cannot crash with a NullReferenceException (since None translates to null).
For example:
let myObject : option<_> = getObjectToUse() // you get a Some<'T>, added explicit typing for clarity
match myObject with
| Some o -> o.DoSomething()
| None -> ... // you have to explicitly handle this case
It is still possible to achieve C#-like behavior, but it is less intuitive, as you have to explicitly say "ignore that this can be None":
let o = myObject.Value // throws NullReferenceException if myObject = None
In C#, you're not forced to consider the case of your variable being null, so it is possible that you simply forget to make a check. Same example as above:
var myObject = GetObjectToUse(); // you get back a nullable type
myObject.DoSomething() // no type error, but a runtime error
Edit: Stephen Swensen is absolutely right, my example code had some flaws, was writing it in a hurry. Fixed. Thank you!
Let's say I show you a function definition like this:
val getPersonByName : (name : string) -> Person
What do you think happens when you pass in a name of a person who doesn't exist in the data store?
Does the function throw a NotFound exception?
Does it return null?
Does it create the person if they don't exist?
Short of reading the code (if you have access to it), reading the documentation (if someone was kindly enough to write it), or just calling the function, you have no way of knowing. And that's basically the problem with null values: they look and act just like non-null values, at least until runtime.
Now let's say you have a function with this signature instead:
val getPersonByName : (name : string) -> option<Person>
This definition makes it very explicit what happens: you'll either get a person back or you won't, and this sort of information is communicated in the function's data type. Usually, you have a better guarantee of handling both cases of a option type than a potentially null value.
I'd say option types are much more benevolent than nulls.
In F# its a big deal that they do not have null values and do not want to support it. Still the programmer has to make cases for None similar to C# programmers having to check != null.
Is None really less evil than null?
Whereas null introduces potential sources of run-time error (NullRefereceException) every time you dereference an object in C#, None forces you to make the sources of run-time error explicit in F#.
For example, invoking GetHashCode on a given object causes C# to silently inject a source of run-time error:
class Foo {
int m;
Foo(int n) { m=n; }
int Hash() { return m; }
static int hash(Foo o) { return o.Hash(); }
};
In contrast, the equivalent code in F# is expected to be null free:
type Foo =
{ m: int }
member foo.Hash() = foo.m
let hash (o: Foo) = o.Hash()
If you really wanted an optional value in F# then you would use the option type and you must handle it explicitly or the compiler will give a warning or error:
let maybeHash (o: Foo option) =
match o with
| None -> 0
| Some o -> o.Hash()
You can still get NullReferenceException in F# by circumventing the type system (which is required for interop):
> hash (box null |> unbox);;
System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicFunctions.UnboxGeneric[T](Object source)
at <StartupCode$FSI_0021>.$FSI_0021.main#()
Stopped due to error

Resources