Skip to main content

New site for Dart news and articles

For the latest Dart news, visit our new blog at  https://medium.com/dartlang .

Dart 1.22: Faster tools, assert messages, covariant overrides

Dart 1.22 is now available. It introduces a sync/async union type, assert messages, covariant parameter overrides, and much more. Tool startup is now much faster. Get it now!

Faster tool startup

We have switched to using application snapshots for running our SDK tools like dart2js, analyzer, and pub. This improves startup performance. See the AOT compiling talk at Dart Dev Summit 2016 for more information. Information about how to use application snapshots can be found in the SDK wiki.

Here are the improved performance numbers we see with the switch.


Assert messages

The fail-fast principle is crucial for building high-quality software, and assert is the simplest way to fail fast. But until now, it wasn’t possible to attach messages to asserts, so if you wanted to make your error useful, you were forced to throw a full exception. Like this:

num measureDistance(List waypoints) {
  if (waypoints.any((point) => point.isInaccessible)) {
    throw new ArgumentError('At least one waypoint is inaccessible.');
  }
  // ...
}

With messages in asserts, your code is not only shorter:

num measureDistance(List<Place> waypoints) {
  assert(waypoints.any((point) => point.isInaccessible),
         'At least one waypoint is inaccessible.');
  // ...
}

But, more importantly, asserts are completely skipped in production, so your production code will be faster (it won’t have to iterate over waypoints at the start of every measureDistance() call).

Covariant parameter override

In object-oriented class hierarchies, especially in user interface frameworks, it's fairly common to run into code like this:

class Widget {
  void addChild(Widget widget) {...}
}

class RadioButton extends Widget {
  void select() {...}
}

class RadioGroup extends Widget {
  void addChild(RadioButton button) {
    button.select();
    super.addChild(button);
  }
}

Here, a RadioGroup is a kind of widget. It refines the base Widget interface by stating that its children must be RadioButtons and cannot be any arbitrary widget. Note that the parameter type in RadioGroup.addChild() is RadioButton, which is a subclass of Widget.

This might seem innocuous at first, but it's actually statically unsound. Consider:

Widget widget = new RadioGroup(); // Upcast to Widget.
widget.addChild(new Widget());    // Add the wrong kind of child.

Tightening a parameter type, that is, using a proper subtype of the existing one in an overriding definition, breaks the Liskov substitution principle. A RadioGroup doesn't support everything that its superclass Widget does. Widget claims you can add any kind of widget to it as a child, but RadioGroup requires it to be a RadioButton.

Breaking substitutability is a little dubious, but in practice it works out fine. Developers can be careful and ensure that they only add the right kinds of children to their RadioGroups. However, because this isn't statically safe, many languages disallow it, including Dart strong mode. (Classic Dart permits it.)

Instead, users must currently manually tighten the type in the body of the method:

class RadioGroup extends Widget {
  void addChild(Widget widget) {
    var button = widget as RadioButton;
    button.select();
    super.addChild(button);
  }
}

The declaration is now statically safe, since it takes the same type as the superclass method. The call to select() is safe because it's guarded by an explicit as cast. That cast is checked and will fail at runtime if the passed widget isn't actually a RadioButton.

In most languages, this pattern is what you have to do. It has (at least) two problems. First, it's verbose. Many users intuitively expect to be able to define subclasses that refine the contracts of their superclasses, even though it's not strictly safe to do so. When they instead have to apply the above pattern, they are surprised, and find the resulting code ugly.

The other problem is that this pattern leads to a worse static typing user experience. Because the cast is now hidden inside the body of the method, a user of RadioGroup can no longer see the tightened type requirement at the API level.

Both problems are solved by covariant parameter overrides. To enable them on a method parameter, you mark it with the contextual keyword covariant:

class Widget {
  void addChild(covariant Widget widget) {...}
}

Doing so says "A subclass may override this parameter with a tighter desired type". A subclass can then override it like so:

class RadioGroup extends Widget {
  void addChild(RadioButton button) {
    // ...
  }
}

No special marker is needed in the overriding definition. The presence of covariant in the superclass is enough. The parameter type in the base class method becomes the original type of the overridden parameter. The parameter type in the derived method is the desired type.

This approach fits well when a developer provides a library or framework where some parameter types were designed for getting tightened. For instance, the Widget hierarchy was designed like that.

In cases where the supertype authors did not foresee this need, it is still possible to tighten a parameter type by putting the covariant modifier on the overriding parameter declaration.

The covariant modifier can also be used on mutable fields. Doing so corresponds to marking the parameter in the implicitly generated setter for that field as covariant:

class Widget {
  covariant Widget child;
}

This is syntactic sugar for:

class Widget {
  Widget _child;
  Widget get child => _child;
  set child(covariant Widget value) { _child = value; }
}

Learn more about this feature in the changelog or in the informal spec.

The Null type is now a subtype of every other type

In other words, the Null type has been moved to the bottom of the type hierarchy. The null literal was always treated as a bottom type. Now the named class Null is too:

final empty = <Null>[];

String concatenate(List<String> parts) => parts.join();
int sum(List<int> numbers) => numbers.fold(0, (sum, n) => sum + n);

concatenate(empty); // OK.
sum(empty); // OK.

FutureOr<T>

A lot of asynchronous code in Dart allows the use of a T or a Future<T>. For example, the callback to Future.then is declared to take a T but it doesn't specify the return type, since it could be an S or a Future<S>:

Future<S> then<S>(onValue(T value), { Function onError });

We are adding FutureOr<T> to support this use case. FutureOr<T> represents the union of Future<T> and T. So the signature of then now looks like this.

Future<S> then<S>(FutureOr<S> onValue(T value), { Function onError });

This tightens types in places where dynamic was used before (like with `then` above), and allows strong-mode tools to do better type inference. In non-strong-mode, FutureOr just means dynamic. In other places, where types were already tight but the implementation had to force asynchronous code, it will allow to loosen that requirement.

Generalized tear-offs are going away

Generalized tear-offs are no longer supported, and will cause errors. We updated the language spec and added warnings in 1.21, and are now taking the last step to fully de-support them. They were previously only supported in the VM, and there are almost no known uses of them in the wild.

Use of Function as a class is now deprecated

You can still use Function as a type name, but don’t use it as a class (don’t extend it, implement it, etc.). For example:

// This is deprecated.
class MyFunction extends Function {
  // …
}

But this is still okay

// This is okay.
void myAwesomeMethod(Function callback) {
  // …
}

Read the changelog for more information and additional changes.

This release took one additional week to finish (our regular schedule is a release each 6 weeks) but we think it was worth the wait.