-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Access to initialized final fields inside initializer expressions #28950
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
Comments
We will definitely not make the initializer list have access to the real object. Only giving access to final variables is a little too specific - the next request will likely be to give read-access to non-final fields too (and not unreasonably so). What we could do instead is to let initializer list assignments also introduce a final variable, visible in the rest of the list, that contains the same value as the one just assigned to the field. There are practical problems to solve too. The variable might have the same name as an existing variable: class C {
final x, y;
C(x, y) : x = x * 2, y = x + y; // What does the final `x` mean?
} As for the work-around, you can do that without a factory constructor too, which reduces both the textual overhead and the problem of not being extensible: class GenericProducerWorkaround<T> extends DelegatingStream<T> {
final StreamController<T> _controller;
GenericProducerWorkaround._(this._controller) : super(_controller.stream);
GenericProducerWorkaround() : this._(new StreamController());
} All in all, it's a possible enhancement, but not one I think will be a high priority any time soon. |
References to other initialized final fields provides concise declarations of relationships. We are converting from Typescript where we use an object/functional reactive style. For example we transform immutable data (
The above won't compile because Changing from final objects to getter functions:
doesn't provide the same runtime semantics. Each invocation of A faux immutable pattern that is immutable external to the object falsely suggests that it can/should be changed internally:
Keeping it immutable with nested named constructors is unwieldily:
It's a little cleaner with a factory constructor:
Having developers use a syntactic pattern to declare relationships between Dart final value initializers doesn't follow other contemporary languages. Java's In our code conversion to Dart, this is one of our biggest annoyances since we define almost all data structures as immutable and use stream transformations to shape the data as it flows from the central repository to the U/I. So how about reconsidering this enhancement request's priority? |
For any given constructor, we are currently adding a local variable for every formal parameter (including a final local variable for each initializing final parameter) to the scope:
If we also add a final local variable to the formal parameter initializer scope for each initialized instance variable, making it a compile-time error to access such a variable before the end of the initializer element for the corresponding instance variable, and initializing each of them with the value used to initialize that instance variable, we'd get something that provides access to the values that you wish to use without breaking the rule that initializer lists cannot access So the following would now have a compile-time errors as indicated: class C {
final x, y, z;
C(x1, y1):
x = x1 * y, // Error, accessing `y` before end of initializer `y = ...`.
y = y1 + y, // Error, same reason.
z = x + 1; // OK
} And the following would work as requested: class Obj4 {
final BehaviorSubject<IList<Person>> _personsSubject;
final Stream<IList<Person>> persons;
final Stream<IList<String>> personNames;
final Stream<IList<String>> personNamesSorted;
Obj4():
_personsSubject = new BehaviorSubject<IList<Person>>(seedValue: nil()),
persons = _personsSubject.stream,
personNames = persons.map((ps) => ps.map((p) => p.name)),
personNamesSorted = personNames.map((ns) => ns.sort(toStringOrder));
} This would probably not be very hard to implement, and we have preserved the protection of One real problem with this approach remains, though, because it aggravates an issue which is already known: If any piece of code captures one of these specialized local variables then the code capturing the variable may be very error-prone, because it does not reflect future updates to the corresponding instance variable: class A {
var x, y;
A(this.x) : y = (() => x);
}
main() {
A a = new A(2);
a.x = 3;
Expect.equals(a.x, 3);
Expect.equals(a.y(), 2);
} (This source code is from this test.) The point is that the function literal captures Another issue to think about is that we may well have a huge number of name clashes: class A {
int x;
A(int x): this.x = x ?? 42;
} It might be a massively breaking change to introduce the rules mentioned above, because the Alternatively, it would surely be massively confusing if we were to avoid all these conflicts by only introducing the new local variables in cases where there is no such name clash. So we might be able to do something about this pretty easily, but it might not be so easy to introduce the feature into the universe of existing Dart code. |
@eernstg - Your proposal to "add a final local variable to the formal parameter initializer scope for each initialized instance variable" sounds promising. As you show with your I see your point about aggravating the issue shown by the Name clashes - yes, the formal parameters shouldn't be shadowed by the initialized values. A name alias could be introduced on either the formal parameter or the initialized value, but I don't think there's a precedent for that. Another possibility (I think I've seen somewhere else) is to allow local variable creation into the initialization scope. Your name clash example could look like:
Since the RHS sees the formals first, the This enhancement request is titled "Access to initialized final fields inside initializer expressions". If the general case is untenable, a solution that focuses on |
@rich-j, maybe the most robust solution would actually be your idea: allow an initializer list element to be a local variable declaration. Then we wouldn't need to introduce those implicitly generated local variables to "shadow" the instance variables at all (they are tricky and confusing, anyway), it would certainly be at least as expressive and flexible as the implicitly generated "shadow" variables (for example, developers can create as many of these 'initializer list locals' as they want), and there is no breakage (in particular: no name clashes in existing code). The only issue I can see is that it would often be a little bit more verbose: class A {
int x, y;
// Using 'initializer list locals'.
A.n1(int x): final _tmpX = x ?? 42, x = _tmpX, y = _tmpX + 2;
// Using implicitly induced "shadows" of instance variables.
A.n2(int x0): x = x0 ?? 42, y = x + 2;
} |
Quoting lrhn
Why exactly? Speaking bluntly, it is annoying and has gotten in my way more than once, like just a few minutes ago. I ended up creating a mutable variable in the class needing the value and then passed the value (a function reference) to it after the initializer list. What problem does it solve? Especially when other languages are fine with it. |
The problem this solves is to avoid giving code access to the object before it has been fully initialized. Java may be "fine" with this, but that choice leads to stackoverflow questions and subtle bugs. C++ takes a different approach, and calls in a constructor will not see subclass virtual overrides of virtual methods called by the constructor. That too leads to subtle bugs. Dart splits constructor calls into two passes: field initialization and the constructor bodies, where the field initialization performed by the initializer list is complete before any constructor body is run. This ensures that no code sees a At times, it would be convenient to be able to do more computation in the initializer list, and that's something we can work on without giving up on the clean separation between initialization and So, Dart is more restrictive here, but also less likely to allow something with a subtle bug caused by calling a virtual method before the object has been completely initialized. |
That's great that you all are trying to protect the users of Dart from these bugs, but I question how relevant that is in the real world. The question you linked is an academic one, and does not seem to correspond to an actual issue the person had. In the last 10 years I have worked on large Java projects in the enterprise as well as Javascript projects and other languages. I can't remember a single time where having the real object available in the constructor (or initializer list. I'm using the two interchangeably in this context) caused a bug in dev, int, stage, or production as a story made its way through the tiers. I do understand that it has the opportunity to cause a bug, but I'm arguing that it rarely plays out in the real world, and guarding against it causes more inconvenience for the developer by requiring him or her to do workarounds, which could lead to other unforeseen bugs. |
If you try to use StreamView or Delegating* classes (from the async package) as a superclass you can hit some REALLY awkward syntax:
The implementation would involve having initialized finals as a variable in initializer scope AFTER the initialization. This would make
C(int x) : z = y * 2, y = x * 2;
(assuming y and z are declared final) ill-formed, since the initializer for z tries to access y, which is uninitialized.The text was updated successfully, but these errors were encountered: