Dart null safety doesn't work with class fields

26,994

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;
}
Share:
26,994
Timmmm
Author by

Timmmm

Updated on August 23, 2022

Comments

  • Timmmm
    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
    shawnblais over 3 years
    It 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
    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
    jamesdlin over 3 years
    @shawnblais Or create a local reference to the member value.
  • shawnblais
    shawnblais over 3 years
    Thanks @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
    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
    shawnblais over 3 years
    That 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
    Adnan about 3 years
    That 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
    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 a var 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 create getSomethig() and setSomehing() 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
    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
    julemand101 about 3 years
    @Adnan In Java, it is standard practice to very rarely make class variables public. Instead we define getVariable() and setVariable() 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
    koldoon about 3 years
    This 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
    julemand101 about 3 years
    @koldoon The problem is exactly that variables behaves exactly like a property with a get (and set) 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 class A define a normal final variable and class B then extending class A by overwriting the get method of the variable defined in class A. Seen from the A's point of view, we should be allowed to promote the variable to non-null just by checking once.
  • julemand101
    julemand101 about 3 years
    But 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 class B. And when you extend A, 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.