dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.28k stars 1.58k forks source link

Final fields null checks aren't handled properly #59586

Closed Hari-07 closed 4 days ago

Hari-07 commented 4 days ago
class Person {
  final String? name;
  Person([this.name]);

  String printName() {
    if (name != null) return name!;
    return 'User';
  }
}

void main() {
  final a = Person();
  a.printName();
}

Consider this code for example. When the class is initialized since name is final, it can be determined statically that for this instance of the object the field name can't be changed anymore since its a final.

So then in the printName method, why does the analyzer require me to assert its non null

julemand101 commented 4 days ago

Since Person are a public class, it can be overwritten in a later subclass (in another library that imports your code). Since final values really just means this value only have a getter, we are allowed to override Person here with:

class SuspiciousPerson extends Person {
  bool first = true;

  @override
  String? get name {
    if (first) {
      first = false;
      return 'James Bond';
    } else {
      return null;
    }
  }
}

void main() {
  final a = SuspiciousPerson();
  a.printName();
  // Unhandled exception:
  // Null check operator used on a null value
}

Because of this, we cannot ensure that name are going to have the same value if you ask for it twice. The solution are instead here to only ask for it once, and then reuse this value like:

  String printName() {
    if (name case final name?) return name;
    return 'User';
  }

Here, we declare a new non-null variable called name that contains the value of the class variable name in case it is not null. See: https://dart.dev/language/pattern-types#null-check

Alternative, if the name variable in your class are defined as private (_name) then it can't no longer be overridden by an unknown subclass, and Dart can therefore ensure the stability of the field and make this valid:

class Person {
  final String? _name;
  Person([String? name]) : _name = name;

  String printName() {
    if (_name != null) return _name;
    return 'User';
  }
}
mraleph commented 4 days ago

@julemand101's explanation in on point.

@Hari-07 please refer to https://dart.dev/tools/non-promotion-reasons to understand limitations of promotion.

Hari-07 commented 3 days ago

Thank you both, that does make sense. I hadn't thought of a final as a field that just has a getter but not a setter, and therefore isn't guaranteed to return the same value in a given instance but it does make sense. Very interesting