2017-11-10 @floitschG
Welcome to the Dart Language and Library Newsletter.
Dart has many ways to make writing constructors easier or more powerful. The most known is probably the concise syntax for initializing instance fields directly in the signature line (see below). This section shows some other, less known features.
// Concise syntax for initializing fields while declaring parameters. class A { final int x; A(this.x); }
A constructor is “generative”, if it is called on a freshly created instance to initialize the object. This sounds complicated, but just describes the behavior of the most common constructors.
class A { int x; A(int y) : this.x = y + 2; }
When a user writes new A()
, conceptually, the program first instantiates an uninitialized object of type A
, and then lets the constructor initialize it (set the field x
).
The reason for this wording is, that generative constructors can be used in super
calls in initializer lists. When called as super
the generative constructor doesn't instantiate a new object again. It just does its part of the initialization.
class A { int x; A(int y) : this.x = y + 2; } class B extends A { B(int z) : super(z - 1) { print("in B constructor"); } }
The order of evaluation is well defined: first all expressions in the initializer list are evaluated. Then the initializer list of the super constructor is run. This continues, until Object
(the superclass of every class) is reached. Then, the bodies of the constructors are executed in reverse order, first starting the one from Object
(not doing anything), and working its way down the class hierarchy.
This evaluation order is usually not noticeable, but can be important when the expressions have side-effects, and/or the bodies read final fields:
int _counter = 0; class A { final int aCounter; A() : aCounter = _counter++ { print("foo: ${foo()}"); } } class B extends A { final int bCounter; final int field; B() : field = 499, bCounter = _counter++ { print("B"); } int foo() => field; } main() { var b = new B(); print("aCounter: ${b.aCounter}"); print("bCounter: ${b.bCounter}"); }
Running this program yields:
foo: 499 B aCounter: 1 bCounter: 0
Note that the bCounter
expression is evaluated first, yielding 0
, and that aCounter
, coming second, is set to 1
. Furthermore, the final field field
in B
is set to 499
when the constructor in A
indirectly accesses the field.
Dart guarantees that final fields are only visible with their final value. Dart ensures this property by splitting the construction of objects into two: the initializer list, and the constructor body. Without this two-phase initialization Dart wouldn't be able to provide this guarantee.
Factory constructors are very similar to static functions, except that they can be invoked with new
. They don't work on an instantiated (uninitialized) object, like generative constructors, but they must create the object themselves.
The following example shows how Future.microtask
could be implemented with a factory
and the existing Completer
class.
class Future<T> { factory Future.microtask(FutureOr<T> computation()) { Completer c = new Completer<T>(); scheduleMicrotask(() { ... c.complete(computation()) ... }); return c.future; } }
The actual implementation uses private classes to be more efficient, but is otherwise very similar to this code.
Factory constructors cannot be used as targets of super
in initializers. (This also means that a class that only has factory constructors cannot be extended).
When constructors want to share code it is often convenient to just forward from one constructor to another one. This can be achieved with factory
constructors, but if the constructor should also be usable as the target of a super
-initializer call, then factory
constructors (as described above) are not an option. In this case, one has to use redirecting generative constructors:
class Point { final int x; final int y; Point(this.x, this.y); } class Rectangle { int x0; int y0; int x1; int y1; Rectangle.coordinates(this.x0, this.y0, this.x1, this.y1); Rectangle.box(Point topLeft, int width, int height) : this.coordinates(topLeft.x, topLeft.y, topLeft.x + width, topLeft.y.height); } class Square extends Rectangle { Box(Point topLeft, int width) : super.box(topLeft, width, width); }
The Rectangle
class has two constructors (both generative): coordinates
and box
. The box
constructor redirects to the coordinates
constructor.
As can be seen, a subtype, here Square
, can still use the constructor in the initializer list.
Frequently, factory constructors are just used to instantiate a differently named class. For example, the Iterable
class is actually abstract
and a new Iterable.empty()
can't therefore be generative but must be a factory. With factory constructors this could be implemented as follows:
abstract class Iterable<E> { factory Iterable.empty() { return new _EmptyIterable<E>(); } }
There are two reasons, why we are not happy with this solution:
new Iterable.empty()
should just directly create an _EmptyIterable
. (Our compilers inline these simple constructors, so this is not a real problem in practice).const
. Clearly, there is code being executed (even if it's just new _EmptyIterable()
), which is not allowed for const
constructors.The solution is to use redirecting factory constructors:
abstract class Iterable<E> { const factory Iterable.empty() = _EmptyIterable<E>; }
Now, the Iterable.empty()
constructor is just a synonym for _EmptyIterable<E>
. Note that we don't even need to provide arguments to the _EmptyIterable<E>
constructor. They must be the same as the one of the redirecting factory constructor.
Another example:
class C { final int x; final int y; const C(this.x, this.y); factory const C.duplicate(int x) = _DuplicateC; } class _DuplicateC implements C { final int x; int get y => x; const _DuplicateC(this.x); }
In Dart it is now easier to declare mutable locals, than to declare immutable variables:
var mutable = 499; final immutable = 42;
Declaring a variable as mutable, but not modifying it, isn't a real problem per se, but it would be nice, if the var
keyword actually expressed the intent that the variable will be modified at a later point.
We recently looked at different ways to make immutable locals more appealing. This section contains our proposal.
Instead of using a different keyword (like val
) we propose to use an even shorter syntax for immutable locals: colon-equals (:=
).
In this proposal, a statement of the form identifier := expression;
introduces a new final local variable.
// DateTime.toString() method. String toString() { y := _fourDigits(year); m := _twoDigits(month); d := _twoDigits(day); h := _twoDigits(hour); min := _twoDigits(minute); sec := _twoDigits(second); ms := _threeDigits(millisecond); us := microsecond == 0 ? "" : _threeDigits(microsecond); if (isUtc) { return "$y-$m-$d $h:$min:$sec.$ms${us}Z"; } else { return "$y-$m-$d $h:$min:$sec.$ms$us"; } }
As a first reaction, it feels dangerous to just use one character (“:”) to introduce a new variable. In our experiments this was, however, not an issue. In fact, single-character modifiers of =
are already common: x += 3
is also just one character on top of =
and we are not aware of any readability issues with compound assignments. Furthermore, syntax highlighting helps a lot in ensuring that these variable declarations aren't lost in the code.
We would also like to support typed variable declarations: Type identifier := expression
. (The following examples are just random variable declarations of our codebase that have been rewritten to use the new syntax).
int pos := value.indexOf(":"); JSSyntaxRegExp re := pattern; IsolateEmbedderData ied := isolateEmbedderData.remove(portId);
For now, we are only looking at the :=
syntax for local variables. If it proves to be successful, we will investigate whether we should allow the same syntax for final (global) statics or fields.
For loops are another place where users frequently declare new variables. There, we need to pay a bit more attention. For example, the for-in statement doesn't even have any assignment symbol, which we could change to :=
.
When looking at uses of for-in, we found that these loops are almost never used without introducing a loop variable:
var x; for (x in [1, 2]) { print(x); }
In fact, the only cases where we found this pattern was in our own tests...
We thus propose to change the meaning of for (identifier in Iterable)
. It should become syntactic sugar for for (final identifier in Iterable)
.
Note that Dart already supports final identifier
in for-in loops, since each iteration has its own variable. This can be seen in the following example:
main() { var funs = []; for (final x in [1, 2, 3]) { // With or without `final`. funs.add(() => x); } funs.forEach((f) => print(f())); // => 1 2 3 }
With the new syntax the final
keyword wouldn't be necessary in this example.
Finally, we also had a look at for
. Similar to for-in, a for
loop, already now, does not reuse the loop variable, but introduces a fresh variable for each iteration.
main() { var funs = []; for (int i = 0; i < 3; i++) { funs.add(() => i); } funs.forEach((f) => print(f())); // => 0 1 2 }
This means that there is already syntactic sugar happening to make this happen. It is thus relatively straightforward to support a version where a loop variable introduced with :=
is final within the body of the loop.
main() { var funs = []; for (i := 0; i < 3; i++) { funs.add(() => i); } funs.forEach((f) => print(f())); // => 0 1 2 }
This would be (roughly) equivalent to:
main() { var funs = []; var i_outer; for (i_outer = 0; i_outer < 3; i_outer++) { i_inner := i_outer; funs.add(() => i_inner); } funs.forEach((f) => print(f())); // => 0 1 2 }
We are investigating ways to make the declaration of final locals easier. In this proposal we suggest the use of :=
as new syntax to concisely declare a fresh final local.
We also propose changes to the for
and for-in statements to make the declaration of final variables concise. The for
loop would support the :=
syntax, and a for-in statement without var
or type would implicitly introduce a fresh final variable.