Description
Solution to #542.
Proposal
Allow local variable initializers to refer to the variable inside a nested function expression. (There is no need for a top-level access, that will always be an error).
If the variable is late
, use normal late
variable semantics.
If the variable is not late
, throw on any access happening before the initializer expression has completed.
Rationale
Dart does not allow a local variable initializer to refer to the variable.
It also doesn't allow an instance variable initializer to refer to the variable because the initializer expression does not have access to this
.
However, static variables can refer to themselves because they are in the global scope. They are also lazily initialized, and lazy initialization already has semantics for accessing itself during initialization. (Dart 1/2.0 semantics is to throw on cyclic initialization error, NNBD will recurse on the initialization, then throw on the second attempted store if final - it does worry me that we evaluate an initializer expression twice, but you really are asking for it if it happens).
We have defined lazy initialization for late
local variables, but have not changed the scoping rules, so it's still an error to be self-referential. If we change the scoping rules, you can write;
late final sub = stream.listen((o) { .... sub.cancel(); ... });
without issues, the semantics are already defined. You then have to read sub
to force the initialization to happen.
Also, with late finals, you can already write:
late final StreamSubscription<String> sub;
sub = stream.listen((o) { ... sub.cancel(); ...});
which is annoying, but avoids the "cannot refer to yourself" without actually improving the safety of anything. (Our definite assignment analysis cannot determine whether sub
is initialized or not at sub.cancel()
, but using late
we turn that into a run-time issue).
With that in mind, we can also allow any local variable to refer to itself, with "throw on early access" semantics. It would mean that the variable becomes more expensive, but only for accesses inside the initializer, all other accesses can assume that the variable is initialized.
It is basically equivalent to a rewrite from ... x = ...;
into late ... x; x = ...;
, which you can do anyway, and definite assignment should recognize that any later access to x
is definitely initialized.
So, we would allow something which may fail, but which we already allow by splitting into two lines, and that is what everybody does anyway, so we are not saving anybody from run-time errors, merely making them write more code.