Every Built-in Method: Iterable

After this lesson you will know what an Iterable and an Iterator really are, why map and where do nothing until somebody loops over them, why looping twice does the work twice, and you will have seen every public member of Iterable, Iterator, the IterableExtensions / NullableIterableExtensions extensions, the constructors Iterable.generate / empty / withIterator / castFrom, and sync* generators, each with its cost, its exceptions and a runnable example whose exact output was checked against the real Dart SDK.

Source of truth: the Dart SDK 3.11 sources (core/iterable.dart, core/iterator.dart, collection/iterable.dart and the implementation classes in _internal/iterable.dart). Costs describe the native VM / AOT. Every printed result is produced by running the code with Dart 3.11 and asserted in verify/d25.dart. Related lessons: D08 collections, D22 mutability, D26 List, D27 Set & Map, D28 Queue & linked structures.

1. What an Iterable and an Iterator are (memory picture)

Think of a ticket dispenser at a bank. The dispenser (the Iterable) can hand out numbered tickets, but it is not the pile of tickets itself. To be served you ask it for a personal bookmark (the Iterator) and then keep asking "next, please": the bookmark moves one step each time and shows the current ticket. Two people each get their own bookmark, so one cannot disturb the other. A List is a dispenser that really holds a shelf of tickets; map or where give you a dispenser that describes how to make tickets when asked and holds none.

Words defined once. An Iterable is any object that can give you its elements one at a time (lists, sets, map keys, the result of map, a generator ...). Its only required member is the getter iterator. An Iterator has two members: moveNext() (step forward; true if there is an element, false at the end) and current (the element you are standing on). The heap is the big shared storage room for objects; the stack is the small scratch area of the running function; a reference is an arrow from a variable to an object.

Every for (final x in it) loop is built from exactly these two members:


  

Output (Dart 3.11):


  

Watch the objects: the list and the new iterator object that xs.iterator creates, with the fields it keeps. Type your own numbers (up to 6, whole numbers -999..999) and press Run. Input size → feasible: a loop over n elements makes n+1 moveNext() calls, so n = 106 takes only a few milliseconds on a typical machine (a rough figure, not a promise).

Key point. Iterable has only one abstract member (iterator); all the others (map, where, length, contains ...) are written once in Iterable itself in terms of it. So to make your own collection usable everywhere you extend Iterable (or mix it in) and write the iterator. The Iterable() constructor is const, IterableBase and IterableMixin are just aliases of Iterable in 3.11, and there is no BidirectionalIterator in Dart 3.11 (it does not exist in dart:core or dart:collection), so you cannot walk backwards with a standard iterator: use List.reversed or an index.

A custom iterable in full: a class with one getter and a small iterator class (compare with the card for Iterable() below):


  

Output (Dart 3.11):



  
  

2. Laziness: one element at a time, and again from scratch

An assembly line that only moves when the last worker pulls. The last worker (toList, a for loop ...) says "give me one"; the worker before passes the request back, step by step to the first. One item travels the whole line, is checked (where) and changed (map), and arrives. Nothing is made in advance, nothing is stored on the way, and when the work is done the line is empty again: the next order starts from raw materials.

The operations that return an Iterable (map, where, take, skip, takeWhile, skipWhile, expand, followedBy, whereType, cast, indexed, nonNulls) are lazy: they build a small wrapper object that remembers its source and your function. The operations that return a plain value or a collection (toList, toSet, length, first, fold, join, any ...) are eager (also called terminal): they pull elements now.


  

Output (Dart 3.11):


  

Nothing printed before doubled.first; first ran map for one element only; the second use ran everything again. Now the full chain. The first player pulls through where, map and take one element at a time. The pipeline text is yours to edit: stages where P, map F, take N, skip N, takeWhile P, skipWhile P, expand dup|pair, followedBy [..], keep (a stored toList()), then terminals such as toList, first, last, length, any P, fold, join (P = even, odd, >N, <N, =N; F = +N, -N, *N, sq). The source is a list of up to 10 numbers (-99..99), gen N (Iterable.generate, N ≤ 8, optionally gen 5 sq) or nat (an infinite generator). Input size → feasible: a chain of s stages over n elements costs about s·n callback calls per terminal run: n = 106, s = 3 is ~3·106 calls (fine); asking elementAt in a loop over a lazy chain is O(n²).

Why the third round stops early. take keeps a counter. When it hits 0 it answers "finished" without calling moveNext on its source, so the stages before it stay idle. That is what lets take(4) tame an infinite generator, and why where still tests the elements that fail it.

Re-iteration recomputes. A lazy chain keeps no results. Every terminal operation starts from the source again, so the same callbacks run again. The counters restart at each terminal so you can compare runs. Second player: three terminals on the same chain; third player: keep (a stored toList()) in the middle pays the cost once.

Efficient length. Some wrappers know their size without running anything. Over a List, map, take and skip keep the list's length and index access, so length, last and elementAt touch very little. where, takeWhile, skipWhile and expand cannot know their length, so they must iterate. Real counts from the SDK:


  

Output (Dart 3.11):


  

A lazy view is live, a snapshot is not. Because a lazy chain holds a reference to its source and no data, changes to the source show up on the next loop; toList() freezes the answer. Type a list and two numbers to add (the filter keeps elements greater than 1):

Pitfalls. (1) Side effects inside map/where run at surprising times and possibly several times. (2) A chain stored in a field and used many times repeats its work: call toList() once. (3) length on a lazy where chain is O(n) and runs your function. (4) A lazy chain over a list sees later changes (section 4).

3. Every member, grouped by purpose

Each card shows the signature, what it means in plain words, a badge telling you what kind of result it gives, its cost and why, the exceptions, and a runnable example with its exact output. No member of Iterable changes the iterable it is called on (the interface has no add or remove): every member either returns a new lazy wrapper, builds a new object, or reads/consumes elements and returns a value.

BadgeMeaning
lazy viewreturns a new lazy Iterable: a wrapper, nothing is computed yet; reads its source when iterated
builds newcomputes now and builds a new List, Set or String; the original is untouched
consumeswalks the elements now (often stopping early) and returns a value
reada property or call that just reads or reports
createsa constructor, factory, static helper or alias that makes an iterable or custom iterable
Coverage. Extracted from the SDK sources with a script: 53 public members are covered, each with its own card: 4 constructors of Iterable (Iterable(), generate, withIterator, empty), 3 statics (castFrom, iterableToShortString, iterableToFullString), 31 instance members of Iterable, 2 members of Iterator (moveNext, current), 5 from IterableExtensions (indexed, firstOrNull, lastOrNull, singleOrNull, elementAtOrNull), 1 from NullableIterableExtensions (nonNulls), the aliases IterableBase and IterableMixin, the 4 members inherited from Object (==, hashCode, runtimeType, noSuchMethod), plus wait (an extension on Iterable<Future> from dart:async). BidirectionalIterator does not exist in Dart 3.11: a search of the whole SDK finds no such type, so it has no card. The extensions live in dart:collection but dart:core re-exports them, so firstOrNull and indexed work without an import.
Index of every member (click a name to jump to its card)

3.1 Create

Ways to get an iterable without a list: write your own class, generate elements from an index, wrap an iterator factory, share the empty one, or view another iterable under a different type. Input size → feasible: Iterable.generate(106, f) stores nothing and costs one call of f per element read; List.generate would allocate 106 slots first.

3.2 Iterator and custom iterables

The protocol itself: iterator, moveNext, current, and the aliases for writing your own iterable. The most famous way it bites is ConcurrentModificationError: the iterator remembers the length it saw and compares it before every step. Type a list and the value that triggers xs.add(99) (use 1,2,3 ; 2). Input size → feasible: the check is O(1) per step; the fix (loop over xs.toList()) costs one O(n) copy.

3.3 Lazy transformers

Each returns a new lazy Iterable. They differ in what they do to the element stream: map converts, where/whereType/nonNulls filter, expand flattens, followedBy concatenates, indexed pairs with positions, cast only re-labels the type. Input size → feasible: chains cost O(stages) per element; expand over n elements producing k each is n·k outputs.

cast versus whereType on mixed data (numbers and words up to 5 letters; use 1,2,x,4):

3.4 Slicing

Take or drop from the start, by count or by condition. Input size → feasible: take(k) reads only k elements even from an infinite source; skip(k) over a lazy chain walks k elements on the first pull (O(k)); over a list it is O(1).

3.5 Ask questions

These consume elements and return one value. Most short-circuit: they stop as soon as the answer is known, which matters on lazy chains because the callbacks stop running too. Input size → feasible: one scan of n = 106 takes only milliseconds (rough); contains n times on a list of n = 105 is 1010 steps (too slow): put the elements in a Set (lesson D27) first.

3.6 Reduce

Squash all elements into one value, or run an action for each. Input size → feasible: all are O(n) calls of your function; a join over n = 106 pieces uses one StringBuffer and is linear (repeated s += x would be quadratic, see lesson D24).

3.7 Convert and print

toList and toSet are the "freeze it now" operations; toString and the two static helpers produce text. Input size → feasible: O(n) time and space; calling toList() inside a loop that runs n times over n elements is O(n²), so convert once outside the loop (lists: D26, sets: D27).

3.8 Inherited from Object

Iterable does not override == and hashCode, and it inherits runtimeType and noSuchMethod. In practice this means: do not compare two iterables with == to compare their contents.

3.9 Extension from dart:async

3.10 sync* generators: write a lazy iterable as a function

A bookmark in a recipe binder. A normal function is a recipe you cook from start to finish in one go. A sync* function is a recipe where, at each yield, you put a bookmark in, hand over one dish and walk away; when the next dish is requested you open the binder at the bookmark and continue with all your ingredients exactly as you left them. Calling the function does not start cooking; asking for dishes does.

A sync* function returns an Iterable. Its body runs only while somebody pulls, pausing at each yield value and resuming right after it. yield* other hands out every element of another iterable (or a recursive call). Every new loop gets a fresh run of the body from the top. (Its asynchronous cousin async* makes a Stream: lesson D14.)


  

Output (Dart 3.11):


  

Player 1: a generator that prints from its body, consumed by first and then length (type n from 0 to 5; with n = 0 first throws). Player 2: yield* delegation with a stack of generator frames. Players 3 and 4: an infinite generator naturals() tamed by take and takeWhile. Input size → feasible: a generator costs O(1) memory however long the sequence; deep yield* recursion costs O(depth) per element.

4. Mutable vs immutable

"Mutable" means an object can change after it is made (lesson D22 explains the idea in depth). The Iterable interface is read-only: it has no mutating member. What can change is the source underneath a lazy view, or a collection you got from toList().

SituationWhat happens
Lazy view over a list, then the list changesthe next loop sees the change (it holds a reference to the source)
toList() / toSet()an independent shallow copy: new list, but the same element objects (nested lists are shared)
toList(growable: false)fixed-length list: add/remove throw UnsupportedError, elements can still be replaced
const list/const Iterable.empty()canonical immutable object; a lazy view over it is allowed (map reads only), but add on the const list throws
Add/remove on a list or set while looping over itConcurrentModificationError on the next step; loop over toList() or use removeWhere
map.keys, map.values, set.where(...)live views of the collection, see lessons D27 and D26

  

Output (Dart 3.11):


  

  

Output (Dart 3.11):


  
Pitfall. rows.map((r) => r).toList() copies the outer list only: copy[0] and rows[0] are the same inner list. Copy the inner lists too (List.of) for a deep copy.

5. Choosing the right tool

ToolStores elements?lengthindex [i]Loop twiceUse it when
lazy Iterable (map, where, sync*)noO(1) if wrappers over a list, else O(n)no (elementAt O(i))recomputes everythingone pass, large or infinite data, pipelines
List (D26)yes (array)O(1)O(1)cheap re-readyou need indexes, repeated reads, sorting
Set (D27)yes (hash table)O(1)nocheap re-readuniqueness, fast contains
Queue/LinkedList (D28)yesO(1)no (Queue: O(1) at both ends)cheap re-readFIFO/stack/deque, fast ends
Stream (D14)no-nosingle-subscription: oncevalues arriving over time
manual Iteratorno-no-two sequences advanced together (merge, zip)

Rules of thumb: build the chain lazily, end it once with toList()/toSet()/a value, and reuse that result. Prefer isEmpty/isNotEmpty to length == 0, firstOrNull to try/catch around first, any to counting, and fold to reduce when the input can be empty.

Quiz

Interview questions

Cheat sheet: every member

Kinds: lazy view = returns a new lazy Iterable; builds new = a new List/Set/String now; consumes = walks elements now; read = property; creates = constructor/alias. Iterable members never mutate the receiver.