Dart null safety doesn't work with class fields
Solution 1
The problem is that class fields can be overridden even if it is marked as final
. The following example illustrates the problem:
class A {
final String? text = 'hello';
String? getText() {
if (text != null) {
return text;
} else {
return 'WAS NULL!';
}
}
}
class B extends A {
bool first = true;
@override
String? get text {
if (first) {
first = false;
return 'world';
} else {
return null;
}
}
}
void main() {
print(A().getText()); // hello
print(B().getText()); // null
}
The B
class overrides the text
final field so it returns a value the first time it is asked but returns null
after this. You cannot write your A
class in such a way that you can prevent this form of overrides from being allowed.
So we cannot change the return value of getText
from String?
to String
even if it looks like we checks the text
field for null
before returning it.
Solution 2
An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.
It seems like this really does only work for local variables. This code has no errors:
class Foo {
String? _a;
void foo() {
final a = _a;
if (a != null) {
a += 'a';
_a = a;
}
}
}
It kind of sucks though. My code is now filled with code that just copies class members to local variables and back again. :-/
Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.
Ah so it turns out a "field initializer" is actually like this:
class Bar {
Bar() : _a = 'a';
String _a;
}
Timmmm
Updated on August 23, 2022Comments
-
Timmmm almost 2 years
I have migrated my Dart code to NNBD / Null Safety. Some of it looks like this:
class Foo { String? _a; void foo() { if (_a != null) { _a += 'a'; } } } class Bar { Bar() { _a = 'a'; } String _a; }
This causes two analysis errors. For
_a += 'a';
:An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.
For
Bar() {
:Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.
In both cases I have already done exactly what the error suggests! What's up with that?
I'm using Dart 2.12.0-133.2.beta (Tue Dec 15).
Edit: I found this page which says:
The analyzer can’t model the flow of your whole application, so it can’t predict the values of global variables or class fields.
But that doesn't make sense to me - there's only one possible flow control path from
if (_a != null)
to_a += 'a';
in this case - there's no async code and Dart is single-threaded - so it doesn't matter that_a
isn't local.And the error message for
Bar()
explicitly states the possibility of initialising the field in the constructor. -
shawnblais over 3 yearsIt seems odd to me that a fairly esoteric use case like overriding final in a sub-class, is now putting a big giant wart in NNBD implementations. Flutter layouts will be full of
!
each one creating a potential bug down the road, because the alternative is just too verbose. -
julemand101 over 3 years@shawnblais If you are sure the variable can never be null at the point of execution but the variable needs to be nullable because of late initialization, you can use the
late
keyword. -
jamesdlin over 3 years@shawnblais Or create a local reference to the member value.
-
shawnblais over 3 yearsThanks @jamesdlin I believe that is actually the only safe way to work with this. But I think most devs will just use the ! operator, which then basically kills any advantage of NNBD for that var, and creates brittle methods that could easily be broken in the future. eg;
if(index == null) return; index = index! + 1;
If someone later removes this null check, the compiler says nothing, and a grenade is sitting just there. In this example that looks a little silly, but on methods longer than a few lines, this is a significant issue, those!
operators do not exactly jump out at the reader. -
julemand101 over 3 years@shawnblais The good thing with
!
is still that it actually makes a null check and fails if null. This failure will therefore happen at the line where the null assignment happens instead of having a wild null object in your code and the need of analyzing and debugging to find the origin of. -
shawnblais over 3 yearsThat is a good thing for sure, but you're still left with holes that the compiler can't see, and bug thats may only occur under specific situations, only detectable at runtime. That's very much not a good thing. If dart team adds some sort of implicit ! to the remainder of the references in a method, this would be a much better solution imo, which they are considering: github.com/dart-lang/language/issues/1188
-
Adnan about 3 yearsThat makes sense for the getter methods, but how can a variable change exactly after the null check? or can't dart distinguish getter method from variable?
-
julemand101 about 3 years@Adnan Exactly. The purpose of getter/setter methods is that they are not distinguish from a normal variable. In a sense, a
final
variable is a field with just a getter while avar
variable both have a setter and getter. This also means that we can always override a variable with a new e.g. getter like my example which is the core of this issue. (but this is also the reason why we don't creategetSomethig()
andsetSomehing()
methods like Java because we can always introduce logic to an existing field if we need it without the need of changing the API. -
Adnan about 3 years@julemand101 can you elaborate more about what do you mean by saying
like Java because we can always introduce logic to an existing field if we need it without the need of changing the API.
I tried the following code in Java and it works just fine: public class Person { public String name; } public class Main { public static void main (String[]args) { Person p = new Person(); p.name = "a"; p.name = "not a"; System.out.println (p.name); } } -
julemand101 about 3 years@Adnan In Java, it is standard practice to very rarely make class variables public. Instead we define
getVariable()
andsetVariable()
to get and set a private variable. The reason is that Java does not have the concept of getter/setter methods (like Dart or C#) so if we later want to introduce e.g. validation of a field, we cannot do that, without changing the API, unless we from the start have used a method to access the variable. -
koldoon about 3 yearsThis example has nothing common with the original question. In the example we explicitly define return type as "String?" which means we must check that it is not a null. on the other hand, in a question we have already checked the field value but the compiler is still complaining. The only reason I see why this happens is that class field member has hidden get (and set) implementation that can return different value each access time, so compiler always treats fields as methods with all the consequences.
-
julemand101 about 3 years@koldoon The problem is exactly that variables behaves exactly like a property with a
get
(andset
) methods. Because of this, we are always allowed in a sub-class to overwrite these methods like any other method. My example illustrate that problem by making classA
define a normal final variable and classB
then extending classA
by overwriting theget
method of the variable defined in classA
. Seen from theA
's point of view, we should be allowed to promote the variable to non-null just by checking once. -
julemand101 about 3 yearsBut because we can extend the class, we can change this behavior and make the null-check invalid in such a way that we gets an error in class
A
because of something we have done in classB
. And when you extendA
, you don't know if the fields, you are overriding, are used in such that we get null-errors if we change the behavior. This usability problem is rather nasty and is the reason why the analyzer requires you to make a local copy of the class field you want to promote before a promotion can happen.