skip to content

In Dart, what does it mean that const values are canonicalized, and which expressions may appear in a const initializer?

level: middleimportance: should knowfreq 45%

answer

  1. one instance per distinct constant
  2. identical() proves it
  3. constant context drops inner const
  4. literals, constants, numeric arithmetic
  5. no ordinary function calls

basics

~20 s

Dart evaluates const expressions at compile time and keeps one instance per distinct constant value, so equal constants are identical(). A const initializer may use literals, other constants, arithmetic on constant numbers, is/as, collection if, spreads and const constructor calls, not arbitrary function calls.

solid answer

~40 s

A **constant expression** is one the compiler can evaluate: literals, other `const` variables, arithmetic and comparisons on constant numbers, string interpolation of constant primitives, `is` and `as`, collection `if` and spreads in constant collections, and `const` constructor calls with constant arguments. Calls like `DateTime.now()` or `double.parse(...)`, instance members, and non-const `final` variables are not allowed. Because the value is fixed, Dart **canonicalizes** it: every occurrence of an equal constant refers to the same object, so `identical(const [1, 2], const [1, 2])` is `true`, while two non-const `[1, 2]` literals are two objects. Inside a **constant context**, such as a const variable's initializer or a const collection, inner `const` keywords are implied and Effective Dart says not to repeat them. Canonicalization saves allocations and makes `identical()` a cheap equality check for constants.

code

dart · 15 lines
dart
const metersPerMile = 1609.344;
const unit = 'km';
const label = '$unit/h'; // interpolation of a const String
const isLong = metersPerMile > 1000; // const comparison

void main() {
  const a = [1, 2];
  const b = [1, 2];
  final c = [1, 2];
  print(identical(a, b)); // true: one canonical instance
  print(identical(a, c)); // false: c is a new list
  print(a == c); // false: List == is identity
  print('$label $isLong');
  // const parsed = double.parse('3.2'); // error: not a constant expression
}

go deeper

for a junior

Remember that const values are fixed at compile time and that equal constants are the same object.

for a middle

List what a constant expression may contain, explain constant contexts, and show identical() versus == on lists.

for a senior

Use constants to cut allocations in hot paths and read the analyzer's const diagnostics quickly when a refactor breaks them.

for a principal

Decide which public API values become constants, since users may put them in switch cases, annotations and defaults, making later changes breaking.

## Constant expressions A **constant expression** is an expression whose value the Dart compiler can compute without running the program. `const` declarations require one; so do switch-case constants, metadata annotations and default parameter values. dart.dev lists what may appear: | Allowed | Example | |---|---| | Literals | `1609.344`, `'km'`, `true`, `null` | | Other constants | `metersPerMile` if declared `const` | | Arithmetic and comparison on constant numbers | `1.01325 * bar`, `metersPerMile > 1000` | | Interpolation of constant primitives | `'$unit/s'` where `unit` is a const String | | Type tests and casts | `i as int`, `i is int` | | Collection `if` and spreads | `[if (metric) 'km', ...baseUnits]` | | Const constructor calls with constant arguments | `const Unit('mi', 1609.344)` | Not allowed: - ordinary function and method calls, such as `DateTime.now()` or `double.parse('3')`; - `final` variables whose values are computed at runtime; - instance members and `this`; - constants from a deferred library. The analyzer's messages name the failure: `const_initialized_with_non_constant_value` for the variable, `const_with_non_constant_argument` when a const constructor receives a runtime value. ## Canonicalization Once the compiler knows a constant's value, there is no reason to build it twice. Dart **canonicalizes** constants: all occurrences of an equal constant value, anywhere in the program, evaluate to **one shared instance**. dart.dev demonstrates it with a const constructor: two `const ImmutablePoint(1, 1)` expressions are `identical`. The same holds for constant lists, maps and sets. 1. `identical(const [1, 2], const [1, 2])` is `true`. 2. `identical([1, 2], [1, 2])` is `false`: each non-const literal allocates a new list. 3. `const [1, 2] == [1, 2]` is `false`, because `List` equality is identity. That last point surprises people who expect structural equality; canonicalization makes *constants* compare equal because they are the same object, not because lists compare by contents. ## Constant context A **constant context** is a place where every expression must be constant anyway: the initializer of a `const` variable, a `const` collection literal, a `const` constructor call's arguments, a metadata annotation, and a switch case expression. Inside it, the `const` keyword is implicit, so ```dart const units = [ ['m', 'km'], ['ft', 'mi'], ]; ``` makes the inner lists constant too. Effective Dart's rule "DON'T use `const` redundantly", enforced by the `unnecessary_const` lint in the recommended set, says not to write the inner keywords. The reverse also matters: **outside** a constant context, calling a const constructor without `const` builds a new, non-canonical object. `var a = const Unit('mi', 1609.344); var b = Unit('mi', 1609.344);` gives two different instances. ## Reading the analyzer's const errors Most const mistakes show up as one of a handful of diagnostics, and each points at a specific fix: - `const_initialized_with_non_constant_value`: the initializer calls a function or reads a runtime value. Compute it differently or use `final`. - `const_with_non_constant_argument`: a const constructor received a runtime argument. Make the argument constant or drop `const` from the call. - `const_with_non_const`: `const` was used on a constructor that is not declared `const`. - `const_instance_field`: an instance field was marked `const`. Make it `static const` or `final`. A refactor that turns a `const` factor into a computed `final` can cascade into several of these, because every constant built from it stops being constant too. ## Why it matters - **Memory and allocation**: a constant used in a hot loop or in many places is allocated once. - **Cheap identity checks**: code that receives constants can compare them with `identical`. - **Required contexts**: switch cases, annotations and default values need constants, so knowing the rules avoids surprise compile errors. ## Boundaries This question covers constant *expressions* and canonicalization. Writing a class with a `const` constructor is part of constructors, the read-only behaviour of constant collections belongs with unmodifiable collections, and why `const` widgets help Flutter rebuilds belongs with Flutter performance.

  • Does calling a Dart const constructor without the const keyword give a canonical instance?
    Only inside a constant context. Elsewhere, `Unit('mi', 1609.344)` without `const` creates a new, non-constant object, so it is not `identical` to `const Unit('mi', 1609.344)`. That is why lints such as `prefer_const_constructors` suggest adding `const` where possible.
  • Why does const [1, 2] == [1, 2] print false in Dart?
    `List` does not override `==`, so equality is identity. The const list is the canonical instance and the other literal is a new list, so they are different objects. Canonicalization only makes two *constants* compare equal, because they are the same object.

saying these in an interview costs you the question

  • Two equal const lists are equal because Dart compares list contents
  • Any expression whose value never changes can initialize a const
  • Inner collections of a const list must each be marked const
  • A const constructor always returns a canonical instance, even without const
  • Canonicalization happens at runtime when the value is first used