Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Null safety feedback: initialization check for late variables #1028

Closed
vsevolod19860507 opened this issue Jun 13, 2020 · 8 comments
Closed
Labels
state-duplicate This issue or pull request already exists

Comments

@vsevolod19860507
Copy link

There should be a way to check if the late variable is initialized or not.
Otherwise, the farther from the place of declaration the initialization occurs, the higher the probability of getting an exception!

void main() {
  final a = A();
  a.init();
  print(a.value); // How can I check if this can be used?
}

// I didn't write this class I use it from some library.
class A {
  late final int value;

  void init() {
    if (_someRemoteAPI()) {
      value = 7;
    }
  }

  bool _someRemoteAPI() {
    return false; // I don't know what will actually returned from here!
  }
}

As a bonus, thanks to this check, we will be able to use late final fields to cache values ​​in immutable classes.

Instead of this

void main() {
  final a = A();
  print(a.value);
  print(a.value);
  print(a.value);
}

class Cache {
  int? data;
}

@immutable
class A {
  final _value = Cache();

  int get value => _value.data ??= longCalculation();

  int longCalculation() {
    print('longCalculation works!');
    return 777;
  }
}

We can use this (The isInitialized function is just an example)

void main() {
  final a = A();
  print(a.value);
  print(a.value);
  print(a.value);
}

@immutable
class A {
  late final int _value;

  int get value {
    if (!isInitialized(_value)) {
      _value = longCalculation();
    }

    return _value;
  }

  int longCalculation() {
    print('longCalculation works!');
    return 777;
  }
}

Also, the analyzer should report at compile time, that we are trying to reinitialize the late final field.

void main() {
  final a = A();
  a.value = 1;
  a.value = 1;
  a.value = 1;
}

@immutable
class A {
  late final int value;
}
@srawlins srawlins transferred this issue from dart-lang/sdk Jun 13, 2020
@eernstg
Copy link
Member

eernstg commented Jun 13, 2020

Note that this was discussed in #324.

@vsevolod19860507
Copy link
Author

vsevolod19860507 commented Jun 13, 2020

But then I think it should be possible to see that this variable is late. When I will use it and hover over it in my editor. (That is, something like a signature) Therefore, I will understand that an exception may occur when using it.

@lrhn
Copy link
Member

lrhn commented Jun 15, 2020

I'd generally discourage using late variables directly in a public API.
A variable being late is an implementation detail that makes sense for local or private variables where the author can ensure that's it's initialized before it's used - because they write all the uses and initializations.

Exposing such a variable to clients of a class means delegating the responsibility of using the variable correctly to the users. If nothing else, I'd prefer to use a StateError for a "use-before-initialization" error of a public API, and hand-craft the error message to tell them what they did wrong.

@lrhn lrhn closed this as completed Jul 3, 2020
@lrhn lrhn added the state-duplicate This issue or pull request already exists label Jul 3, 2020
@om-ha
Copy link

om-ha commented Oct 22, 2021

Some tips I came up with from advice of different dart maintainers, and my self-analysis:

late usage tips:

  • Do not use late modifier on variables if you are going to check them for initialization later.
  • Do not use late modifier for public-facing variables, only for private variables (prefixed with _). Responsibility of initialization should not be delegated to API users. EDIT: as lrhn mentioned, this rule makes sense for late final variables only with no initializer expression, they should not be public. Otherwise there are valid use cases for exposing late variables. Please see his descriptive comment!
  • Do make sure to initialize late variables in all constructors, exiting and emerging ones.
  • Do be cautious when initializing a late variable inside unreachable code scenarios. Examples:
    • late variable initialized in if clause but there's no initialization in else, and vice-versa.
    • Some control-flow short-circuit/early-exit preventing execution to reach the line where late variable is initialized.

Please point out any errors/additions to this.

Enjoy!

Sources:

@lrhn
Copy link
Member

lrhn commented Oct 25, 2021

For

  • Do not use late modifier for public-facing variables, only for private variables (prefixed with _). Responsibility of initialization should not be delegated to API users.

there are situations where it's perfectly reasonable to have an instance variable with a lazily-initialized default value.
The one thing you should not do is to have a late final public instance variable with no initializer. That's a "write once" variable, and you should never make that public. Also don't make a late variable with no initializer public, because someone might read it while uninitialized (and if an error is possible, then it's better to give a good error message for it).

So, basically agree if you add a "with no initializer expression". Those are the dangerous ones. Late variables with initializers are just lazy, but safe.

Not sure what exiting and emerging constructors are.

The exception to the rule above is that you can have uninitialized late instance fields with no initializer if you initialize them in the constructor bodies instead. Generally, that only makes sense when you need to create cyclic references between newly created immutable objects. Otherwise I'd do my darnedest to avoid late variables that are always initialized in the constructor anyway. Seems like it should be unnecessary,

A late variable which is only initialized on some code paths is meaningful, but dangerous. If your logic is consistent, and you only read the variable in cases where you also know it's written, then you're fine, but you can't let users see that.

I guess the short version is: Never let a client of your class see an "uninitialized late" error. If such an error can happen when accessing or writing a late variable (no initializer expression), make the variable private and ensure you only use it correctly, or ensure that it's definitely initialized in the constructor, before any user code sees the object.

Late variables with initializer expressions are always safe.

(If you need to check for initialization, you can use late:

 bool _fooInitialized = false;
 final late Foo? foo = (_fooInitialized = true) ? _createFoo() : ("throw "unreachable");

That's fairly inefficient, because you are effectively duplicating the "initialization state" of the variable, but shorter to write than

 bool _fooInitialized = false;
 Foo? _foo;
 Foo? get foo => _fooInitialized || !(_fooInitialized = true) ? _foo : (_foo = _createFoo());

)

@om-ha
Copy link

om-ha commented Oct 25, 2021

@lrhn Thanks for the valuable explanation! Updated my comment accordingly here, in the other thread, and on SO. Linked your comment as well.

@fullflash
Copy link

it would be nice to have compile time check for uninitialized late variables
like a lint

@mateusfccp
Copy link
Contributor

it would be nice to have compile time check for uninitialized late variables
like a lint

If we could know it at compile time we wouldn't even need it...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
state-duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

6 participants