Collections

Domains: Dart

DO

DO use collection literals when possible.

There are two ways to make an empty growable list[] and List(). Likewise, there are three ways to make an empty linked hash map{}Map(), and LinkedHashMap().

If you want to create a non-growable list, or some other custom collection type then, by all means, use a constructor. Otherwise, use the nice literal syntax. The core library exposes those constructors to ease adoption, but idiomatic Dart code does not use them.

var points = [];
var addresses = {};
var points = List();
var addresses = Map();

You can even provide a type argument for them if that matters.

var points = <Point>[];
var addresses = <String, Address>{};
var points = List<Point>();
var addresses = Map<String, Address>();

Note that this doesn’t apply to the named constructors for those classesList.from()Map.fromIterable(), and friends all have their uses. Likewise, if you’re passing a size to List() to create a non-growable one, then it makes sense to use that.

DO use whereType() to filter a collection by type.

Linter rule: prefer_iterable_whereType

Let’s say you have a list containing a mixture of objects, and you want to get just the integers out of it. You could use where() like this:

var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int);

This is verbose, but, worse, it returns an iterable whose type probably isn’t what you want. In the example here, it returns an Iterable<Object> even though you likely want an Iterable<int> since that’s the type you’re filtering it to.

Sometimes you see code that “corrects” the above error by adding cast():

var objects = [1, "a", 2, "b", 3];
var ints = objects.where((e) => e is int).cast<int>();

That’s verbose and causes two wrappers to be created, with two layers of indirection and redundant runtime checking. Fortunately, the core library has the whereType() method for this exact use case:

var objects = [1, "a", 2, "b", 3];
var ints = objects.whereType<int>();

Using whereType() is concise, produces an Iterable of the desired type, and has no unnecessary levels of wrapping.

 

 

PREFER

CONSIDER

CONSIDER using higher-order methods to transform a sequence.

If you have a collection and want to produce a new modified collection from it, it’s often shorter and more declarative to use .map().where(), and the other handy methods on Iterable.

Using those instead of an imperative for loop makes it clear that your intent is to produce a new sequence and not to produce side effects.

var aquaticNames = animals
    .where((animal) => animal.isAquatic)
    .map((animal) => animal.name);

At the same time, this can be taken too far. If you are chaining or nesting many higher-order methods, it may be clearer to write a chunk of imperative code.

AVOID

AVOID using Iterable.forEach() with a function literal.

forEach() functions are widely used in JavaScript because the built in for-in loop doesn’t do what you usually want. In Dart, if you want to iterate over a sequence, the idiomatic way to do that is using a loop.

for (var person in people) {
  ...
}
people.forEach((person) {
  ...
});

Note that this guideline specifically says “function literal”. If you want to invoke some already existing function on each element, forEach() is fine.

people.forEach(print);

Also note that it’s always OK to use Map.forEach(). Maps aren’t iterable, so this guideline doesn’t apply.

AVOID using cast().

This is the softer generalization of the previous rule. Sometimes there is no nearby operation you can use to fix the type of some object. Even then, when possible avoid using cast() to “change” a collection’s type.

Prefer any of these options instead:

  • Create it with the right type. Change the code where the collection is first created so that it has the right type.

  • Cast the elements on access. If you immediately iterate over the collection, cast each element inside the iteration.

  • Eagerly cast using List.from(). If you’ll eventually access most of the elements in the collection, and you don’t need the object to be backed by the original live object, convert it using List.from().

    The cast() method returns a lazy collection that checks the element type on every operation. If you perform only a few operations on only a few elements, that laziness can be good. But in many cases, the overhead of lazy validation and of wrapping outweighs the benefits.

Here is an example of creating it with the right type:

List<int> singletonList(int value) {
  var list = <int>[];
  list.add(value);
  return list;
}
List<int> singletonList(int value) {
  var list = []; // List<dynamic>.
  list.add(value);
  return list.cast<int>();
}

Here is casting each element on access:

void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (var n in objects) {
    if ((n as int).isEven) print(n);
  }
}
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (var n in objects.cast<int>()) {
    if (n.isEven) print(n);
  }
}

Here is casting eagerly using List.from():

int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = List<int>.from(objects);
  ints.sort();
  return ints[ints.length ~/ 2];
}
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = objects.cast<int>();
  ints.sort();
  return ints[ints.length ~/ 2];
}

These alternatives don’t always work, of course, and sometimes cast() is the right answer. But consider that method a little risky and undesirable—it can be slow and may fail at runtime if you aren’t careful.

DON'T

DON’T use .length to see if a collection is empty.

The Iterable contract does not require that a collection know its length or be able to provide it in constant time. Calling .length just to see if the collection contains anything can be painfully slow.

Instead, there are faster and more readable getters: .isEmpty and .isNotEmpty. Use the one that doesn’t require you to negate the result.

if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

DON’T use List.from() unless you intend to change the type of the result.

Given an Iterable, there are two obvious ways to produce a new List that contains the same elements:

var copy1 = iterable.toList();
var copy2 = List.from(iterable);

The obvious difference is that the first one is shorter. The important difference is that the first one preserves the type argument of the original object:

// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<int>":
print(iterable.toList().runtimeType);
// Creates a List<int>:
var iterable = [1, 2, 3];

// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

If you want to change the type, then calling List.from() is useful:

var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

But if your goal is just to copy the iterable and preserve its original type, or you don’t care about the type, then use toList().

DON’T use cast() when a nearby operation will do.

Often when you’re dealing with an iterable or stream, you perform several transformations on it. At the end, you want to produce an object with a certain type argument. Instead of tacking on a call to cast(), see if one of the existing transformations can change the type.

If you’re already calling toList(), replace that with a call to List<T>.from() where T is the type of resulting list you want.

var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();

If you are calling map(), give it an explicit type argument so that it produces an iterable of the desired type. Type inference often picks the correct type for you based on the function you pass to map(), but sometimes you need to be explicit.

var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => 1 / n);
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => 1 / n).cast<double>();

Similar pages

Page structure
Terms

List

Collections

Map

int

Methods

Functions

Dart

double

String

Constructor

Numbers

Classes