Search code examples
dartdart-null-safety

Why can a nullable value still be considered nullable inside the scope of an if statement that checks for nullability?


Consider this demo:

void test(int? n) {
  if (n != null) { // let's make sure n is not null
    ++n; // ok; n is assumed to be non-null

    if (n < 0) {
      n = n.abs(); // ok; n is not null in inner scope
    }

    while (n != 0) {
      --n; // error: the method '-' can't be unconditionally invoked because the receiver can be 'null'
    }
  }
}

Why is n still considered int? inside the while(...) { ... } block? I am using VSCode with the latest version of Dart.


Solution

  • Unlike the code earlier in your method body, the code in your while loop can be run multiple times. It's possible for you to assign null to n in one of your loop's iterations and, therefore, be possible to call -- on a null object.

    Dart's static analysis is able to see that n will not be null with non-looping code relatively easily, it just has to go through each line to see if there is a possible null assignment. With the loop, Dart's analysis is just not that smart. I'm guessing this detection cannot be done as it would require actually running your loop's code to determine if n could become null again.

    The following loop shows an example of where automatic n cannot be guaranteed to be null, even though n is not assigned to be null prior to --n:

    while (n != 0) {
      --n; // error: the method '-' can't be unconditionally invoked because the receiver can be 'null'
      if(n == 5) {
        n = null;
      }
    }
    

    In order to determine if n can be null at the --n, Dart's analysis would have look ahead to see that n is assigned to null.


    Cases like this where Dart's analysis is not able to accurately determine if a variable could be null is why the bang operator exists:

    void test(int? n) {
      if (n != null) { // let's make sure n is not null
        ++n; // ok; n is assumed to be non-null
    
        if (n < 0) {
          n = n.abs(); // ok; n is not null in inner scope
        }
    
        while (n != 0) {
          n = n! - 1;
        }
      }
    }
    

    Alternatively, you could use a local non-nullable variable for your particular example:

    void test(int? n) {
      if (n != null) { // let's make sure n is not null
        int nLocal = n;
        ++nLocal; // ok; n is assumed to be non-null
    
        if (n < 0) {
          nLocal = nLocal.abs(); // ok; n is not null in inner scope
        }
    
        while (n != 0) {
          --nLocal;
        }
      }
    }