skip to content

In Dart, what is the difference between an Iterable and a List, and why does list.map(...) not give you a List?

level: juniorimportance: must knowfreq 60%

answer

  1. anything you can step through
  2. no [] operator on Iterable
  3. map and where return lazy views
  4. nothing runs until iterated
  5. toList materialises and snapshots

basics

~20 s

A Dart Iterable is any sequence you can step through; a List adds indexing, a stored length and mutation. map, where and similar methods return a lazy Iterable that computes nothing until iterated, so you call toList() to get a List.

solid answer

~40 s

`Iterable<E>` only promises an `iterator`; `List<E>` is an `Iterable` that also offers `list[i]`, a constant-time `length` and, if growable, mutation. On a general `Iterable` there is no `[]` — `elementAt` may step from the start — and `length` may count by iterating. Methods such as `map`, `where`, `expand`, `take`, `skip` and `whereType` return a **new lazy `Iterable`**: calling them does no work, and the callbacks run only when something iterates the result, such as a `for` loop, `toList()`, `first` or `fold`. So `List<String> x = lines.map(f);` is a type error; add `.toList()` to materialise the values, which also lets you index, read a cheap length, iterate repeatedly without recomputation and keep a snapshot independent of the source.

code

dart · 14 lines
dart
void main() {
  final lines = ['INFO start', 'ERROR disk full', 'INFO done'];

  final tags = lines.map((l) => l.split(' ').first);
  print(tags); // (INFO, ERROR, INFO) - an Iterable prints in parentheses

  final tagList = tags.toList();
  print(tagList[1]); // ERROR
  print(tagList); // [INFO, ERROR, INFO]

  for (final (i, line) in lines.indexed) {
    print('$i: $line');
  }
}

go deeper

for a junior

Know that map and where return an Iterable, not a List, and that toList() converts it when you need indexing or a List-typed API.

for a middle

Explain laziness: nothing runs until iteration, length and elementAt may walk the sequence, and toList() snapshots the result.

for a senior

Decide where a pipeline should be materialised and where it should stay lazy, based on how many times and how far it is consumed.

for a principal

Guide API design so functions return Iterable only when laziness is intended and List when callers need stored, stable results.

## Two levels of the collection hierarchy In Dart, **`Iterable<E>`** is the general interface for "a sequence of values you can step through". Its only required member is the `iterator` getter; everything else — `map`, `where`, `contains`, `length`, `first`, `fold` — is defined in terms of stepping through that iterator. `List`, `Set`, the `keys` and `values` of a `Map`, the result of a `sync*` generator and the result of `map` or `where` are all `Iterable`s. **`List<E>`** is a specific `Iterable` that also promises **indexed access** and a known length: | | `Iterable<E>` | `List<E>` | |---|---|---| | Element access | `elementAt(i)`, possibly by stepping from the start | `list[i]`, direct | | `length` | may iterate every element to count them | stored, constant time | | Mutation | none in the interface | `add`, `remove`, `[]=` (if growable) | | Evaluation | may be computed lazily, on every pass | elements are stored | | `toString()` | `(a, b, c)` in parentheses | `[a, b, c]` in brackets | Because an `Iterable` cannot promise cheap random access, it has no `[]` operator; you use `elementAt`, which the Dart docs warn can be linear. ## Why map returns an Iterable `Iterable.map`, `where`, `expand`, `take`, `skip`, `takeWhile`, `skipWhile` and `whereType` all return a **new, lazy `Iterable`**. Nothing is computed when you call them. The work happens only when something iterates the result: a `for-in` loop, `toList()`, `toSet()`, `first`, `fold`, `join`, `contains` and so on. That is why this does not compile: ```dart final lines = ['INFO start', 'ERROR disk full', 'INFO done']; List<String> upper = lines.map((l) => l.toUpperCase()); // error ``` `lines.map(...)` has static type `Iterable<String>`, which is not assignable to `List<String>`. Add `.toList()` to **materialise** the result into a list. `toSet()` does the same into a `LinkedHashSet`, dropping duplicates. ## When to convert with toList Call `toList()` (or `toSet()`) when you need: - an API that demands a `List`, such as a Flutter `Column`'s `children`; - indexing (`result[3]`) or a cheap `length`; - to iterate the result **more than once** without recomputing it; - a **snapshot** that no longer reflects later changes to the source collection; - to mutate the result. Keep the `Iterable` when you only pass it on to one consumer that iterates once — `join`, a single `for` loop, `fold` — or when you might stop early with `first`, `any` or `take`, because then the lazy form avoids work on the elements you never reach. ## A few Iterable-only helpers - `whereType<T>()` filters **and** narrows the static type, so `mixed.whereType<int>()` is an `Iterable<int>`; `where((e) => e is int)` would still be `Iterable<Object>`. - `indexed` (Dart 3.0) yields `(int, E)` records: `for (final (i, line) in lines.indexed) ...`. - `firstOrNull`, `lastOrNull`, `singleOrNull` and `elementAtOrNull` (Dart 3.0) return `null` instead of throwing on a missing element. They are defined in `dart:collection` and exported from `dart:core`, so no import is needed. ## Common mistakes 1. **Assigning `map(...)` to a `List`** — a compile error, fixed with `.toList()`. 2. **Calling `.toList()` by reflex** — harmless for small inputs, but it allocates a list you may never need, for example before a single `join`. 3. **Using `elementAt` in a loop on a lazy `Iterable`** — each call may step from the beginning, making the loop quadratic; convert once with `toList()`. 4. **Expecting `map` to run its callback immediately** — a `map` whose result is never iterated never calls its function at all. ## Interview summary - `Iterable` = anything you can iterate; `List` = an `Iterable` with indexes, a stored length and mutation. - `map`, `where` and friends return **lazy** `Iterable`s; nothing runs until iteration. - `toList()` / `toSet()` materialise, snapshot and allow indexing.

  • Why is calling elementAt(i) inside a loop over a lazy Dart Iterable a performance trap?
    A general `Iterable` has no random access, so `elementAt(i)` may step through `i` elements from the start, and on a lazy chain it reruns the callbacks too. Looping over indexes then costs quadratic time. Iterate directly with `for-in`, or call `toList()` once and index the list.
  • What does whereType<int>() give you in Dart that where((e) => e is int) does not?
    The right static type. `whereType<int>()` returns `Iterable<int>`, while the `where` version keeps the original element type, such as `Iterable<Object>`, and would need a `cast<int>()` wrapper. Effective Dart recommends `whereType` for filtering by type.
  • Does a Dart map() callback run if nobody iterates its result?
    No. `map` only builds a lazy wrapper; the function runs when an element is requested. A `map` written purely for its side effects, with the result discarded, does nothing. Use a `for` loop for side effects.

saying these in an interview costs you the question

  • list.map(...) returns a new List.
  • map runs its callback as soon as it is called.
  • Every Iterable supports iterable[i] indexing.
  • Iterable.length is always constant time.
  • toList() is required before a for-in loop.