skip to content

In Dart, what are function, method and constructor tear-offs such as forEach(print) and map(User.new), and when should a lambda stay instead?

level: middleimportance: should knowfreq 45%

answer

  1. the name without parentheses
  2. top-level, static, instance, constructor
  3. ClassName.new since Dart 2.15
  4. receiver bound at tear-off time
  5. signature must match exactly

basics

~20 s

A tear-off names a function, method or constructor without parentheses, producing a function object that calls it: print, int.parse, buffer.write, User.new. Prefer it over a lambda that only forwards arguments; keep the lambda when arguments must be adapted or the receiver re-read.

solid answer

~40 s

Naming something callable without `()` creates a **tear-off**: a closure with the same parameters that calls the original. Dart supports top-level functions (`print`), static methods (`int.parse`), instance methods (`buffer.write`, bound to that `buffer`) and, since Dart 2.15, constructors: `User.fromJson` for a named one and `User.new` for the unnamed one. Effective Dart says not to write a lambda when a tear-off will do, so `names.map((n) => User(n))` becomes `names.map(User.new)`; the opt-in `unnecessary_lambdas` lint flags such lambdas. Keep the lambda when the signatures differ (extra arguments, a cast such as `(e) => User.fromJson(e as Map<String, dynamic>)`), or when the receiver must be read at call time: `_controller.clear` binds the controller that exists now, while `() => _controller.clear()` uses whichever controller the field holds when called.

code

dart · 21 lines
dart
class User {
  User(this.name);
  User.fromJson(Map<String, dynamic> json) : name = json['name'] as String;
  final String name;
}

void main() {
  final names = ['ana', 'ben'];
  final users = names.map(User.new).toList(); // unnamed constructor tear-off

  final rows = <Map<String, dynamic>>[{'name': 'cy'}];
  final fromRows = rows.map(User.fromJson).toList(); // named constructor

  final List<dynamic> decoded = [{'name': 'dee'}];
  // decoded.map(User.fromJson) would not compile: map expects a dynamic parameter.
  final fromDecoded =
      decoded.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();

  users.map((u) => u.name).forEach(print); // print is a function tear-off
  print(fromRows.length + fromDecoded.length); // 2
}

go deeper

for a junior

Recall that a name without parentheses is a tear-off and that forEach(print) passes print itself.

for a middle

Name the four tear-off kinds including ClassName.new from Dart 2.15, and explain when a signature mismatch forces a lambda.

for a senior

Recognise receiver binding at tear-off time, and choose lambdas deliberately where fields may be reassigned or casts are needed.

for a principal

Decide which style lints, such as unnecessary_lambdas, to enable across the codebase, balancing consistency against churn in existing code.

## What a tear-off is When you write the name of a function, method or constructor **without** calling it, Dart creates a **tear-off**: a function object that takes the same parameters and, when called, invokes the original. The term comes from tearing off the parentheses. Effective Dart's guidance is direct: if a lambda only calls a named function with the same parameters it receives, use the tear-off instead. ## The four kinds | Kind | Example | Calls | |---|---|---| | Top-level function | `codes.forEach(print)` | `print(code)` | | Static method | `texts.map(int.parse)` | `int.parse(text)` | | Instance method | `codes.forEach(buffer.write)` | `buffer.write(code)` on that `buffer` | | Constructor (2.15+) | `names.map(User.new)`, `rows.map(User.fromJson)` | `User(name)`, `User.fromJson(row)` | Before **Dart 2.15** constructors could not be torn off, so code had to write `(x) => A(x)`. Dart 2.15 added constructor tear-offs: a named constructor is torn off by its name, and the unnamed constructor by `ClassName.new`. Outside a tear-off, `.new` is unnecessary: `User.new('ana')` works but the recommended `unnecessary_constructor_name` lint asks for `User('ana')`. ## Why prefer a tear-off - It is shorter and says exactly "call this". - It avoids re-declaring parameters, so it cannot get their types or order wrong. - For a constructor, the dart.dev tour puts it as: a lambda wraps the constructor, whereas a tear-off **is** the constructor. The `unnecessary_lambdas` lint (not in the default sets; enable it in `analysis_options.yaml`) reports closures that could be tear-offs. ## When a lambda must stay A tear-off fits only when its signature matches the expected function type **exactly as written**. Keep the lambda when: 1. **Arguments must be supplied or reordered**: `() => _delete(item)` for a `VoidCallback`. 2. **A cast or conversion is needed**: decoded JSON is `List<dynamic>`, and `map` there expects a function taking `dynamic`. `User.fromJson` takes `Map<String, dynamic>`, so `jsonList.map(User.fromJson)` does not compile; `jsonList.map((e) => User.fromJson(e as Map<String, dynamic>))` does. 3. **Only some parameters should pass through**: `ListView.builder`'s `itemBuilder` receives `(context, index)`; a method taking just `index` needs `(context, i) => _row(i)`. 4. **The receiver must be read at call time** (next section). ## The receiver is bound when you tear off An instance-method tear-off evaluates its **receiver immediately**. `_controller.clear` produces a function bound to the controller object that `_controller` holds **now**. A lambda, `() => _controller.clear()`, reads the field each time it runs. Inside `build` this rarely bites, because a `setState` that replaces the controller also rebuilds the widget and creates a fresh tear-off. It matters when the callback is **stored** somewhere that is not rebuilt: registered with a service, kept in a field, or passed to a long-lived object. If `_controller` is reassigned afterwards, the stored tear-off still targets the old controller, which may already be disposed. The same applies to `this`: `onPressed: _save` binds the current `State` object, which is exactly what you want inside that `State`. ## Tear-offs you meet in Flutter code Flutter APIs expect function types all over, so tear-offs appear everywhere: - `TextField(onChanged: _setName)`: `onChanged` is a `ValueChanged<String>?`, so `_setName` must take one `String`; - `ListView.builder(itemBuilder: _buildRow)`: fits when `_buildRow` takes `(BuildContext, int)` and returns a `Widget`; - `IconButton(onPressed: Navigator.of(context).pop, ...)`: `pop`'s only parameter is optional, so it fits `VoidCallback`, and the navigator is looked up once, when `build` runs; - `items.map(ItemTile.new)`: builds one widget per item when `ItemTile`'s unnamed constructor takes exactly one positional argument. If a constructor also needs a `key` or other named arguments, write the lambda: `(item) => ItemTile(item, key: ValueKey(item.id))`. ## Summary for an interview - Tear-offs exist for top-level functions, static methods, instance methods and, since 2.15, constructors via `ClassName.new` or the named constructor. - Prefer them over forwarding lambdas; the analyzer can flag the lambdas. - Keep a lambda for adapted arguments, casts, dropped parameters or late-bound receivers.

  • Why does Dart's jsonList.map(User.fromJson) fail when jsonList is List<dynamic>?
    `map` on a `List<dynamic>` expects a function whose parameter accepts `dynamic`. `User.fromJson` accepts only `Map<String, dynamic>`, and a function with a narrower parameter type is not a subtype of one that accepts anything. Cast first with a lambda, or cast the list: `jsonList.cast<Map<String, dynamic>>().map(User.fromJson)`.
  • In Dart, when is storing _controller.clear as a callback riskier than storing () => _controller.clear()?
    When the field can be reassigned after the callback is stored somewhere that is not rebuilt, such as a service or a long-lived object. The tear-off is bound to the controller held when it was created and keeps targeting that, possibly disposed, object. The lambda reads the field on every call.

saying these in an interview costs you the question

  • Constructors cannot be torn off in Dart, so map((x) => User(x)) is required
  • User.new calls the constructor immediately and passes the new object
  • A method tear-off looks up its receiver again every time it is called
  • Any function with the right name can be torn off regardless of parameter types
  • unnecessary_lambdas is enabled by default in flutter_lints