16

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.

Timmmm
  • 88,195
  • 71
  • 364
  • 509

4 Answers4

24

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.

julemand101
  • 28,470
  • 5
  • 52
  • 48
  • 11
    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. – shawnblais Mar 10 '21 at 01:44
  • @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. – julemand101 Mar 10 '21 at 06:52
  • 2
    @shawnblais Or create a local reference to the member value. – jamesdlin Mar 10 '21 at 22:27
  • 4
    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. – shawnblais Mar 11 '21 at 21:28
  • @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. – julemand101 Mar 11 '21 at 21:57
  • 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: https://github.com/dart-lang/language/issues/1188 – shawnblais Mar 12 '21 at 02:37
  • 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? – Adnan May 15 '21 at 06:54
  • 1
    @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. – julemand101 May 15 '21 at 07:48
  • @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); } } – Adnan May 15 '21 at 08:09
  • 1
    @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. – julemand101 May 15 '21 at 08:14
  • 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. – koldoon Jun 01 '21 at 06:19
  • @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 Jun 01 '21 at 07:19
  • 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. – julemand101 Jun 01 '21 at 07:22
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
  • 88,195
  • 71
  • 364
  • 509
0

There are few ways to deal with this situation. I've given a detailed answer here so I'm only writing the solutions from it:

  • Use local variable (Recommended)

    void foo() {
      var a = this.a; // <-- Local variable
      if (a != null) {
        a += 'a';
        this.a = a;
      }
    }
    
  • Use ??

    void foo() {
      var a = (this.a ?? '') + 'a';
      this.a = a;
    }
    
  • Use Bang operator (!)

    You should only use this solution when you're 100% sure that the variable (a) is not null at the time you're using it.

    void foo() {
      a = a! + 'a'; // <-- Bang operator
    }
    

To answer your second question:

Non-nullable fields should always be initialized. There are generally three ways of initializing them:

  • In the declaration:

    class Bar {
      String a = 'a';
    }
    
  • In the initializing formal

    class Bar {
      String a;
    
      Bar({required this.a});
    }
    
  • In the initializer list:

    class Bar {
      String a;
    
      Bar(String b) : a = b;
    }
    
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
-1

You can create your classes in null-safety like this

    class JobDoc {
  File? docCam1;
  File? docCam2;
  File? docBarcode;
  File? docSignature;

  JobDoc({this.docCam1, this.docCam2, this.docBarcode, this.docSignature});

  JobDoc.fromJson(Map<String, dynamic> json) {
    docCam1 = json['docCam1'] ?? null;
    docCam2 = json['docCam2'] ?? null;
    docBarcode = json['docBarcode'] ?? null;
    docSignature = json['docSignature'] ?? null;
  }
}
gtr Developer
  • 2,369
  • 16
  • 12