Open natebosch opened 5 years ago
Dart does not allow access to a variable inside its own initialializer. (This is unrelated to cascades, so I've updated the title to the more general situation).
It is usually hit when a closure contains a reference to the variable, and the author knows that the function won't be called immediately.
Other examples include:
var subscription = stream.listen((o) {
if (o == null) subscription.cancel();
...
});
var port = rawReceivePort((o) {
if (o == null) port.close();
...
});
The problem is that if we allow access to the yet-uninitialized variable, we also have to give it a semantics. Dart has steadfastly refused to give access to final variables in a way where you can see them prior to initialization. With NNBD, this becomes even more essential because they might not even have the value null
.
The options are to throw on access prior to initialization, or to give "some value". In either case (and the latter is unlikely, especially for NNBD), it requires an extra check on each access to the variable, which might be an unnecessary overhead.
This is unrelated to cascades
I agree that solving the general problem for an initializer most likely solves it for cascades, however I do think the problem of cascades is a bit narrower and could potentially be solved more easily.
From a user perspective the most straightforward desugaring of a cascade would make the reference to the variable happen outside of its initialization.
You are suggesting that an assignment to a cascade expression would perform the assignment to the receiver value prior to evaluating the cascade sections? That is possible, but somewhat inconsistent. In all other situations the cascade in total before the value becomes available.
It would still expose the object in a state prior to the cascade, which may be an initialization.
E.g., var x = Foo()..init(()=>x)..finalize();
. Here x
is bound to Foo()
pre-finalization, and init has access to that x
variable, so it is possible that the pre-finalized Foo()
leaks. That's a tricky case to debug.
If it's only variable initialization, and not any assignment, then it's inconsistent. It would mean that var x = Foo()..init(()=>x)..finalize();
and var x; x = Foo()..init(()=>x)..finalize();
would have different bindings for x
if init
calls its argument synchronously. That's ... badly inconsistent. But if we are talking all assignments, then x = Foo()..something()..finalize();
where x
is a setter would see the pre-finalized Foo
and might try to do something with it.
All in all, I think changing the evaluation order for cascades is too inconsistent and error-prone to be really viable.
I see what you mean about the assignment to something other than a variable initializer. I think I was implicitly treating variable initialization as a different desugaring to other usages of cascades.
It feels sensible to me to treat var x =
different from any other x =
, but I can see the arguments against that as well.
The duplicated bug I linked is basically this one, although I found it even more confusing because I wasn't trying to refer to the variable explicitly - but this code fails:
const Foo foo = Foo()..baz();
I would have expected that to desugar into:
const Foo foo = Foo();
foo.baz();
which is allowed - and
final Foo foo = const Foo()..resolve();
is also allowed.
Just FYI: The notion of an anonymous method was proposed with, among other things, this particular kind of situation in mind. You could say that it works as a "better cascade". Here's the blub
example expressed using an anonymous method:
// Desired actions.
var foo = bar();
foo.baz(() {
foo.blub();
});
// Same thing using (proposed, not yet implemented) anonymous methods.
var foo = bar()..{ baz(() => blub()); };
The point is that the .{ /*statements*/ }
or ..{ /*statements*/ }
construct contains regular code where the receiver (obtained by evaluating bar()
) can be denoted by this
. In particular, we can call blub()
as shown, as opposed to the situation with a cascade. Of course, this
may be omitted for member accesses (so this.baz()
can be written baz()
).
These properties make the code in .{ /*statements*/ }
or ..{ /*statements*/ }
similar to the code in an instance method of the receiver bar()
, hence the name 'anonymous methods'.
Came across this today with WebViewController
via webview_flutter
.
final WebViewController controller = WebViewController()
.. // ...
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
controller.runJavascript(); // "Local variable 'controller' can't be referenced before it is declared" error
},
),
);
Looks like "anonymous methods" are just Kotlin's apply
scope function? Those functions are super useful! Guessing I'd just be able to do:
final WebViewController controller = WebViewController().{
setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
controller.runJavascript(); // no error?
},
),
);
};
You can implement something like that yourself, as:
extension WithSelf<T> on T {
R apply<R>(R Function(T) onSelf) => onSelf(this);
}
Then you can write:
final WebViewController controller = WebViewController()..apply((controller) {
setNavigationDelegate(
NavigationDelegate(
onPageFinished: (url) {
controller.runJavascript(); // no error?
},
),
);
});
You'd have to name the controller twice (if you need it outside at all).
It would be nice to have a block with bindings that doesn't introduce a new function body. Doesn't have to bind the value to this
, but binding it to something without that being a function parameter.
(Personally I'd like declaration expressions (#1420 or similar), which would allow something like (final controller = WebViewControler())..setNavigariontDelegate(...);
. The scope that declaration would be the rest of the block, any code dominated by the declaration expression, but the parentheses makes the assignment happen before the cascade.)
Cascade syntax helps avoid redundant references to the same variable, but it breaks down once you need to reference that variable within a cascade call.
The following are logically equivalent:
However the same equivalence can't be made with: