Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 6 – Records and pattern matching

Records in Dart: several values without a class

Bundle values into a Dart record, read its fields, see why equal records find the same map entry, and decide when a class or a map fits better.

  • Beginner
  • 15 minutes
  • Examples run with dart 3.13.5 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Create records with positional and named fields and read each field through its getter
  • Explain which parts of a record type make up its shape, and which field names are only documentation
  • Predict when two records are equal, and use records as map keys and set elements
  • Write a function that returns several values in one record and read them at the call site
  • Choose between a record, a class and a map for a piece of structured data

Before you start

What you’ll learn · Before you start

What you will learn

  • Create records with positional and named fields and read each field through its getter
  • Explain which parts of a record type make up its shape, and which field names are only documentation
  • Predict when two records are equal, and use records as map keys and set elements
  • Write a function that returns several values in one record and read them at the call site
  • Choose between a record, a class and a map for a piece of structured data

Before you start

On this page

A function that finds the coldest and the hottest of five readings has two answers, 27 and 35, but a return statement hands back one value. Before Dart 3 that meant a List that forgets which number is which, a Map whose keys nobody checks, or a class written only to carry two ints. A record carries both in one value, with a type the compiler checks and an == that compares contents.

You will build records with positional and named fields and read them through their getters, see which names belong to a record’s type, measure when two records are equal and use them as keys of a seat map, return two values from one function, and decide when a class or a map fits better than a record.

A record holds a fixed set of fields, read through getters

ticket.dart builds one value for a train ticket: the train number and the two stations, plus the coach and the seat by name:

ticket.dart: one record with five fields Dart · ticket.dart
void main() {
  // Three positional fields and two named ones, written in any order.
  final ticket = ('12127', coach: 'B2', 'Pune', seat: 41, 'Mumbai');
  print(ticket);
  print('train ${ticket.$1}, from ${ticket.$2} to ${ticket.$3}');
  print('coach ${ticket.coach}, seat ${ticket.seat}');
}

Output

(12127, Pune, Mumbai, coach: B2, seat: 41)
train 12127, from Pune to Mumbai
coach B2, seat 41

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run ticket.dart

All five values went into one object, written as a comma-separated list in parentheses. That object is a record: An anonymous, immutable value that bundles a fixed set of positional and named fields, each with its own type. Records need no declaration and compare equal when their shape and fields are equal.; the records page calls records an “anonymous, immutable, aggregate type”, since a record needs no declaration, cannot change, and bundles several values. The values written on their own are positional field: A field of a record written without a name and read by its position, as $1, $2 and so on, counting only the positional fields from the left., and the ones written as a name, a colon and a value are named field: A field of a record written as name: value and read through a getter of the same name. Its name is part of the record's type, and the order of named fields is not.. The two kinds may be mixed in any order. Step through how each field is read:

Interactive · step by step Which getter reads each field of the ticket Step through the record from ticket.dart. The left column is the record as written, the right one the getter that reads each field.
Which getter reads each field of the ticket, step 4 of 4. Showing '12127', coach: 'B2', 'Pune', seat: 41, 'Mumbai', ticket.$1, ticket.coach, ticket.$2, ticket.seat, ticket.$3.'12127'coach: 'B2''Pune'seat: 41'Mumbai'ticket.$1ticket.coachticket.$2ticket.seatticket.$3no setter on any field

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Five fields in one record. The record expression lists five fields between parentheses. Three are positional, written as plain values, and two are named, written as a name, a colon and a value. Named and positional fields may be mixed in any order.
  2. Positional fields are numbered. Only positional fields get a number, counted from the left from 1: '12127' is $1, 'Pune' is $2 and 'Mumbai' is $3. The named fields between them take no number, which is why 'Pune' is $2 and not $3.
  3. Named fields keep their names. Each named field is read by a getter with its own name, ticket.coach and ticket.seat. The program printed coach B2, seat 41.
  4. Getters only, no setters. Every field is read through a getter, and there are no setters, so no field can be given a new value. print showed the positional fields first and then the named ones.

Positional fields are read as $1, $2 and $3, numbered among the positional fields only, so ‘Pune’ is $2 even though coach stands between it and the train number. Named fields are read by their names. The printed form put the positional fields first; the specification leaves the order of named fields in that text open and lets optimised builds print records in other ways, so use it for debugging, never as output a user reads.

Every field has a getter and none has a setter. no_setter.dart tries to move a passenger to row C:

no_setter.dart: giving a field of a record a new value Dart · no_setter.dart
void main() {
  final seat = (row: 'B', number: 12);
  seat.row = 'C';
  print(seat);
}

Output (exit status 254)

Printed as an error (standard error)

no_setter.dart:3:8: Error: The setter 'row' isn't defined for the type '({int number, String row})'.
Try correcting the name to the name of an existing setter, or defining a setter or field named 'row'.
  seat.row = 'C';
       ^^^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run no_setter.dart

The compiler stopped with “The setter ‘row’ isn’t defined” and the program never ran, with exit status 254. The type in the message lists number before row: named fields are kept sorted by name, because the order you write them in is not part of the type. To change a field, build a new record with the new value.

Parentheses alone do not make a record. one_field.dart wraps 42 twice, once with a comma:

one_field.dart: parentheses, a trailing comma and an empty record Dart · one_field.dart
void main() {
  final a = (42);
  final b = (42,);
  print('(42) is ${a.runtimeType}, (42,) is ${b.runtimeType}');
  print('b.\$1 = ${b.$1}');
  final nothing = ();
  print('() is ${nothing.runtimeType}');
}

Output

(42) is int, (42,) is (int)
b.$1 = 42
() is ()

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run one_field.dart

(42) is the number 42 in parentheses, an int. (42,) is a record with one positional field, whose type prints as (int) and whose $1 is 42. The trailing comma is needed only for a single positional field: (city: 'Pune') is a record without one, because the name already marks it, and () is the empty record.

A one-value record without its comma

final wrapped = (amount); gives wrapped the type of amount, so wrapped.$1 does not compile. Write (amount,) when you really want a record with one positional field, or name the field: (amount: amount).

Checkpoint

Check: Positional and named fields and their getters

  1. Question 1 of 2 In var r = ('Asha', age: 31, 'Pune', city: 'Pune');, which getter reads the second positional value?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: r.$2

    Positional getters count positional fields only, from 1. Named fields are read by name and never shift the numbers.

  2. Question 2 of 2 Which expression creates a record with a single positional field?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: (42,)

    A record with exactly one positional field needs a trailing comma, so that it is not read as a number in parentheses. A single named field, as in (city: 'Pune'), needs no comma.

A record’s type is its shape, and named fields belong to it

Record types are written the way a function’s parameters are: (int, String) for two positional fields, and ({double lat, double lng}) for named fields, which go in braces after any positional ones. There is no declaration for a record type. The records page says the set of fields, their types and their names make up the record’s record shape: The number of positional fields of a record, the names of its named fields and the type of every field. The shape alone decides a record's type., and the shape alone decides the type. The next two programs test which names count. The first gives Pune’s coordinates to a variable whose fields have other names:

named_shape.dart: the same two doubles under other field names Dart · named_shape.dart
void main() {
  ({double lat, double lng}) pune = (lat: 18.52, lng: 73.86);
  ({double latitude, double longitude}) sameCity = pune;
  print(sameCity);
}

Output (exit status 254)

Printed as an error (standard error)

named_shape.dart:3:52: Error: A value of type '({double lat, double lng})' can't be assigned to a variable of type '({double latitude, double longitude})'.
  ({double latitude, double longitude}) sameCity = pune;
                                                   ^

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run named_shape.dart

The compiler refused it: the value typed ({double lat, double lng}) cannot be stored in a variable typed ({double latitude, double longitude}). Renaming a named field makes another type. Names on positional fields work differently:

field_names.dart: positional fields under swapped names Dart · field_names.dart
void main() {
  (int paise, int quantity) line = (2500, 2);
  // The names in a positional record type are only documentation:
  (int quantity, int paise) relabelled = line;
  print('relabelled.\$1 = ${relabelled.$1}');
  print('same value: ${relabelled == line}');
}

Output

relabelled.$1 = 2500
same value: true

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run field_names.dart

(int paise, int quantity) went into (int quantity, int paise) without a word, and relabelled.$1 still printed 2500, the price. The compiler matched the fields by position, and the names were a note for the reader. So when two positional fields have the same type, nothing stops a caller from reading them the wrong way round; with named fields, every read says which field it means. This table sums up which changes make a new record type:

A change to a record type Another type?
Rename a named field yes
Rename a positional field in an annotation no, those names are documentation
Write the named fields in another order no
Add or remove a field yes
Change the type of a field yes

Key idea

A record’s type is its shape: how many positional fields it has, the names of its named fields and the type of every field. Names written on positional fields are comments the compiler ignores.

A long record type is tiring to repeat, so give it a name with a typedef: A named alias for a type, most often a function type, such as typedef Validator = String? Function(String). It documents intent and keeps declarations readable; it creates no new class. More in “Closures, function types and tear-offs”. txn.dart names the shape of a payment and uses that name in a function’s signature:

txn.dart: a typedef for the shape of a payment Dart · txn.dart
typedef Txn = ({String id, int amountPaise, String vpa});

Txn refund(Txn t) => (id: '${t.id}-R', amountPaise: -t.amountPaise, vpa: t.vpa);

void main() {
  final Txn paid = (id: 'T42', amountPaise: 25000, vpa: 'asha@okbank');
  final back = refund(paid);
  print(back);

  // The alias and the written-out type are one and the same type.
  ({String id, int amountPaise, String vpa}) plain = back;
  print('plain is Txn: ${plain is Txn}');
}

Output

(amountPaise: -25000, id: T42-R, vpa: asha@okbank)
plain is Txn: true

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run txn.dart

refund takes and returns a Txn, and plain is Txn printed true: the alias and the written-out record type are one type, which is how the typedefs page describes every alias. The printout listed the fields as amountPaise, id and vpa, sorted again rather than in the order written.

Payment handles and paise

The vpa field holds a UPI virtual payment address in the name@handle form. Keeping each amount as a whole number of paise in an int, never as rupees in a double, keeps every sum exact.

Checkpoint

Check: The shape of a record type

  1. Question 1 of 2 Which assignment compiles, given p of type ({int x, int y}) and q of type (int a, int b)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: (int b, int a) r = q;

    A record's shape counts positional fields by position and named fields by name. field_names.dart assigned a record to a type with swapped positional names, and named_shape.dart failed to compile with renamed named fields.

  2. Question 2 of 2 After typedef Txn = ({String id, int amountPaise, String vpa});, what is Txn?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Another name for that record type

    A typedef gives a shape a readable name. The alias and the written-out record type are interchangeable everywhere, which is why it adds readability but no protection.

Records with equal fields are equal

equality.dart makes two records for one postcode, one from a literal and one from a sum, and then compares several pairs:

equality.dart: which pairs of records are equal Dart · equality.dart
void main() {
  final pin = 411000 + 1;
  final a = ('Pune', 411001);
  final b = ('Pune', pin);
  print('a == b: ${a == b}');
  print('equal hash codes: ${a.hashCode == b.hashCode}');

  final x = (city: 'Pune', pin: 411001);
  final y = (pin: 411001, city: 'Pune');
  print('named fields in another order: ${x == y}');

  final tags = ['veg', 'jain'];
  print('one list in both: ${('Thali', tags) == ('Thali', tags)}');
  final p = ('Thali', ['veg', 'jain']);
  final q = ('Thali', ['veg', 'jain']);
  print('two equal lists: ${p == q}');

  final order = ('Thali', tags);
  tags.add('no onion');
  print(order);
}

Output

a == b: true
equal hash codes: true
named fields in another order: true
one list in both: true
two equal lists: false
(Thali, [veg, jain, no onion])

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run equality.dart

a == b printed true, and the two hash codes were equal too, although the records are separate objects made in different ways. The records page gives the rule: two records are equal when they have the same shape and their corresponding fields have equal values. That is structural equality: Equality decided by contents rather than by object: two records are equal when they have the same shape and every pair of corresponding fields is equal., and every record gets it, together with a matching hashCode, without a line of code from you. Since the order of named fields is not part of the shape, the third comparison printed true as well.

The fourth and fifth lines look alike but disagree. Step through what == did for p == q, the pair that printed false:

Interactive · step by step What == compares on two records Step through p == q from equality.dart. Each step compares one pair of fields, the left record against the right one.
What == compares on two records, step 4 of 4. Showing record p, record q, p.$1 'Thali', q.$1 'Thali', p.$2 [veg, jain] (list object 1), q.$2 [veg, jain] (list object 2). In focus: record p, record q.same shape?String ==List ==record precord qp.$1 'Thali'q.$1 'Thali'p.$2 [veg, jain]list object 1q.$2 [veg, jain]list object 2p == q is false

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Same shape first. p and q each hold two positional fields, a String and a List of strings, so they have the same shape. That is the first thing == checks: a record with other fields, or other names on its named fields, is never equal to p.
  2. The first pair is equal. Then == compares each pair of fields with the == of the field's own value. String == compares the characters, and 'Thali' is equal to 'Thali'.
  3. Two lists, two objects. Each list literal built a list of its own. A List is equal only to itself, whatever its elements, so this pair is not equal although both lists hold veg and jain.
  4. One unequal pair decides. A single unequal pair makes the records unequal, and p == q printed false. When both records held the one tags list, their second fields were the same object, and the comparison printed true.

A record compares its fields with each field’s own ==, so the result is only as good as that operator. List’s documentation says lists “are, by default, only equal to themselves”, which is reference identity: Whether two references lead to one and the same object. identical() checks it and no class can change its answer; == checks it too unless the class overrides ==. More in “Operators, cascades and equality in Dart”: one shared list made the records equal, two lists with the same elements did not. The last line shows a second consequence. After tags.add('no onion'), the record printed the new element: a record cannot point a field at another object, but the object a field holds can still change.

Treating a record as a frozen copy of a list

final order = ('Thali', tags); does not copy tags, so every later tags.add(...) shows up in order. When the record must keep the values it had, build it from a copy of the list instead of the list itself.

Checkpoint

Check: When two records are equal

  1. Question 1 of 2 What does ('Thali', ['veg']) == ('Thali', ['veg']) give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: false

    A record compares each pair of fields with the field's own ==. Strings compare their characters, but lists compare identity, so equal-looking lists make the records unequal.

  2. Question 2 of 2 Is (city: 'Pune', pin: 411001) equal to (pin: 411001, city: 'Pune')?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Yes, the order of named fields is not part of the shape

    Two records are equal when they have the same shape and equal corresponding fields. Named fields are matched by name, so their written order never matters.

Equal records find the same map entry

A cinema keeps its bookings in a map from seat to name, and the key of each entry is a record with a row and a seat number. seat_map.dart books two seats, looks them up with records built later, and collects picked seats in a set:

seat_map.dart: seats as map keys and set elements Dart · seat_map.dart
void main() {
  final booked = <({String row, int seat}), String>{};
  booked[(row: 'B', seat: 12)] = 'Asha';
  booked[(row: 'B', seat: 13)] = 'Ravi';

  // A new record with equal fields finds the same entry.
  final wanted = (row: 'B', seat: 6 * 2);
  print('B12 is booked by ${booked[wanted]}');
  print('B13 taken: ${booked.containsKey((seat: 13, row: 'B'))}');
  print('B14 taken: ${booked.containsKey((row: 'B', seat: 14))}');

  final picked = {
    (row: 'C', seat: 1),
    (row: 'C', seat: 1),
    (row: 'C', seat: 2),
  };
  print('${picked.length} different seats: $picked');
}

Output

B12 is booked by Asha
B13 taken: true
B14 taken: false
2 different seats: {(row: C, seat: 1), (row: C, seat: 2)}

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run seat_map.dart

Each lookup in the output built a new record, and each found the entry stored earlier, or none:

Lookup or action Printed
booked[(row: 'B', seat: 6 * 2)] Asha
containsKey((seat: 13, row: 'B')), named fields swapped true
containsKey((row: 'B', seat: 14)) false
a set literal with C1 twice and C2 once 2 different seats

The LinkedHashMap documentation explains why this works: a map literal such as booked is a LinkedHashMap, which finds entries in a hash table, so its keys need an == and a hashCode that agree, with equal hash codes for equal keys. The records specification requires exactly that of records, which makes any record whose fields compare soundly, such as strings and ints, a dependable key. The seat (row: 'B', seat: 6 * 2) was computed at run time and still matched, and the set dropped the repeated C1 without being told to.

Checkpoint

Check: Records as map keys and set elements

  1. Question 1 of 2 booked holds an entry for (row: 'B', seat: 12). What does booked[(seat: 12, row: 'B')] return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The stored name

    A map literal is a hash table that needs keys whose == and hashCode agree. Records give exactly that, so any equal record finds the entry.

  2. Question 2 of 2 How many elements does {(1, 'a'), (1, 'a'), (2, 'a')} hold?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 2 elements

    The two (1, 'a') records are equal and hash alike, so the set keeps one of them, as the seat set kept C1 once.

One function can return several values in a record

Back to the five readings from the start. min_max.dart returns the lowest and the highest reading together:

min_max.dart: a function that returns two ints at once Dart · min_max.dart
(int, int) minMax(List<int> xs) {
  var lo = xs.first, hi = xs.first;
  for (final x in xs.skip(1)) {
    if (x < lo) lo = x;
    if (x > hi) hi = x;
  }
  return (lo, hi);
}

void main() {
  final temperatures = [31, 27, 35, 29, 33];
  final (lo, hi) = minMax(temperatures);
  print('lowest $lo, highest $hi');

  final range = minMax(temperatures);
  print('spread ${range.$2 - range.$1} degrees');
}

Output

lowest 27, highest 35
spread 8 degrees

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run min_max.dart

The return type (int, int) promises exactly two ints, and return (lo, hi); builds the record. At the call site, final (lo, hi) = minMax(temperatures); declares two variables in one statement and printed lowest 27, highest 35. That declaration is a pattern, which the destructuring lesson takes apart in full. The second call keeps the whole record and reads it through $1 and $2, which gave the spread of 8 degrees.

The records page puts the alternatives in one sentence: a class is more verbose, and a List or a Map loses type safety. This table compares the ways the same two results can come back:

Return type The caller receives What the compiler checks
List<int> [27, 35] that each value is an int, not how many there are or which is which
Map<String, int> {'min': 27, 'max': 35} nothing about the keys
a class MinMax an object with two fields everything, after you declare the class
(int, int) (27, 35) the number of values and the type of each
({int min, int max}) (min: 27, max: 35) the number, the types and the names

The last row is the safest when both values have the same type, because a caller writes range.min and cannot mix them up. A record also groups futures of different types so that they can be awaited together, which the asynchronous programming module shows.

Checkpoint

Check: Several return values in one record

  1. One question minMax returns (int, int). Which line reads both values at the call site?

    Choose one answer.

    Show the answer: One question

    Answer: final (lo, hi) = minMax(xs);

    A record pattern in a declaration splits the returned record into variables in one statement. Without it, read the fields through $1 and $2.

A class or a map fits when a record cannot

Records only hold data. as_map.dart stores Pune’s coordinates in a map and in a record, and asks both the same questions:

as_map.dart: the same coordinates in a map and in a record Dart · as_map.dart
void main() {
  // The same two values as a map: the keys are strings, checked only at run time.
  final pune = <String, double>{'lat': 18.52, 'lng': 73.86};
  print('misspelt key: ${pune['lattitude']}');

  final again = <String, double>{'lat': 18.52, 'lng': 73.86};
  print('two equal maps ==: ${pune == again}');

  // As a record, the same misspelling would not compile, and equal values are equal.
  final asRecord = (lat: 18.52, lng: 73.86);
  print('two equal records ==: ${asRecord == (lat: 18.52, lng: 73.86)}');
}

Output

misspelt key: null
two equal maps ==: false
two equal records ==: true

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run as_map.dart

The misspelt key 'lattitude' compiled and quietly read null, and two maps with equal entries compared false, because Map keeps the identity == that List has. The record would refuse the misspelling at compile time, and its two equal values compared true. A class sits between them: it is checked like a record, and it can also run code, such as a constructor that refuses a negative amount, which a record never can. Answer the three questions in the table for a piece of data from your own code:

Interactive · answer and compare Record, class or map for your data? Answer the three questions about your data. Options that cannot work are ruled out, each with the reason.
Does the data need methods, a check when a value is made, or a rule that must always hold?
Will code outside your library depend on this type for a long time?
Do you know every field name when you write the code?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

A record, a class and a map against five things that structured data may need
Shape Checked by the compiler Equal by contents Methods, checks, rules A public type that grows Keys known at run time
Record Holds: fixed fields, each typed Good every field name and type is checked, as named_shape.dart showed Good == and hashCode are built in: the two records compared true Poor no methods of its own and no constructor that can refuse a value Poor another field is another shape, so another type Poor the fields are fixed when the code is written
Class Holds: fields and code Good fields and getters are checked, as on a record Mixed only identical objects are equal, until you override == and hashCode Good a constructor can refuse bad values, and methods keep the rules in one place Good a new getter leaves code that uses the old ones compiling Poor the fields are fixed when the class is written
Map Holds: keys chosen at run time Poor a misspelt key compiles and reads null, as as_map.dart printed Poor two maps with equal entries compared false Poor nothing runs when an entry is added or changed Mixed new keys compile anywhere, and nothing checks that they are there Good any key, added or removed while the program runs
How the answers decide
  • “Does the data need methods, a check when a value is made, or a rule that must always hold?” Yes, it needs code: rules out Record and Map. Only a class runs your code when a value is made or used, for example to refuse a negative amount.
  • “Will code outside your library depend on this type for a long time?” Yes, it is a public type: adds a caveat to Record. dart.dev notes that only a class gives full abstraction and encapsulation; a typedef only names a record shape.
  • “Do you know every field name when you write the code?” No, they come from data: rules out Record and Class. Names that arrive with the data need a map, because a record or a class fixes its names when it is compiled.
  • “Does the data need methods, a check when a value is made, or a rule that must always hold?” No, it only holds values and “Will code outside your library depend on this type for a long time?” No, it stays local and “Do you know every field name when you write the code?” Yes, they are fixed: adds a caveat to Class. A class works too, but for values that only travel together a record needs no declaration and brings its own ==.

In short: a record for values that only travel together inside your code, such as results, keys and pairs; a class when the data needs methods, checks or a long-lived public type; a map when the field names arrive with the data. The records page adds that a typedef only names a record’s shape, and that only a class gives full abstraction and encapsulation.

Checkpoint

Check: Record, class or map

  1. Question 1 of 2 An amount must never be negative, and every value must be checked when it is made. Which shape fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A class with a constructor that checks the amount

    The trade-off table rules out records and maps for data that needs a check or a rule: only a class can enforce one.

  2. Question 2 of 2 The field names arrive with the data, and the code cannot know them in advance. What should hold the data?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A map

    Records and classes have names fixed at compile time. Names known only at run time need a map, with the checks that come with it.

Practise: two functions that return records

The package has three files: a pubspec whose one dev dependency is package:test, a library whose two functions still throw UnimplementedError, and a test file whose four sample tests say what those functions must return.

The records package: two stubs and four sample tests

practice/pubspec.yaml

name: practice
description: Two functions that return records, with four sample tests.
publish_to: none

environment:
  sdk: ^3.13.0

dev_dependencies:
  test: ^1.33.0

lib/practice.dart · practice/lib/practice.dart

/// The smallest value, the largest value and the mean of [xs],
/// or null when [xs] is empty.
({int min, int max, double mean})? stats(List<int> xs) {
  throw UnimplementedError('stats');
}

/// Splits a handle such as 'asha@okbank' into its user part and its
/// handle part, or returns null when [vpa] does not follow the rule:
/// exactly one '@', with at least one character on each side.
(String user, String handle)? splitVpa(String vpa) {
  throw UnimplementedError('splitVpa');
}

test/practice_test.dart · practice/test/practice_test.dart

import 'package:practice/practice.dart';
import 'package:test/test.dart';

void main() {
  test('stats of three readings', () {
    expect(stats([4, 1, 7]), (min: 1, max: 7, mean: 4.0));
  });

  test('stats of no readings is null', () {
    expect(stats([]), isNull);
  });

  test('a handle splits at the @', () {
    expect(splitVpa('asha@okbank'), ('asha', 'okbank'));
  });

  test('a handle without a user part is refused', () {
    expect(splitVpa('@okbank'), isNull);
  });
}

The session prints the starter file and then runs the tests:

The starter package and its four failing sample tests Python · practice_session.py
from session import files, run

# The practice package as handed out: both functions still throw, so all four sample tests fail.
files("practice=records_practice")
run(
    """
cd records_practice
cat lib/practice.dart
dart pub get --offline > /dev/null
!dart test --reporter failures-only
""",
    pub=True,
)

Output

$ cd records_practice
$ cat lib/practice.dart
/// The smallest value, the largest value and the mean of [xs],
/// or null when [xs] is empty.
({int min, int max, double mean})? stats(List<int> xs) {
  throw UnimplementedError('stats');
}

/// Splits a handle such as 'asha@okbank' into its user part and its
/// handle part, or returns null when [vpa] does not follow the rule:
/// exactly one '@', with at least one character on each side.
(String user, String handle)? splitVpa(String vpa) {
  throw UnimplementedError('splitVpa');
}
$ dart pub get --offline > /dev/null
$ dart test --reporter failures-only
+0 -1: test/practice_test.dart: stats of three readings [E]
  UnimplementedError: stats
  package:practice/practice.dart 4:3  stats
  test/practice_test.dart 6:12        main.<fn>

+0 -2: test/practice_test.dart: stats of no readings is null [E]
  UnimplementedError: stats
  package:practice/practice.dart 4:3  stats
  test/practice_test.dart 10:12       main.<fn>

+0 -3: test/practice_test.dart: a handle splits at the @ [E]
  UnimplementedError: splitVpa
  package:practice/practice.dart 11:3  splitVpa
  test/practice_test.dart 14:12        main.<fn>

+0 -4: test/practice_test.dart: a handle without a user part is refused [E]
  UnimplementedError: splitVpa
  package:practice/practice.dart 11:3  splitVpa
  test/practice_test.dart 18:12        main.<fn>

+0 -4: Some tests failed.

Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

  1. Copy the package into a folder of your own: pubspec.yaml, lib/practice.dart and test/practice_test.dart as above. Run dart pub get and then dart test: all four tests fail, as in the session.
  2. Write stats. Return null for an empty list; otherwise build a record with the named fields min, max and mean, where mean is a double.
  3. Write splitVpa by the rule in its comment: exactly one ‘@’, with at least one character before and after it. Return a record of the two parts, or null.
  4. Run dart test until it ends with “All tests passed!”. Then add tests of your own for one reading, for negative readings, and for ‘a@’, ‘a@b@c’ and an empty string, and make them pass.

Once all four sample tests pass, check what stayed with you from each section:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 In ticket.dart, how is the seat number read from the record?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: ticket.seat

    Named fields are read through getters with their own names, positional fields through $1, $2 and so on.

  2. Question 2 of 12 A record seat = (row: 'B', number: 12) must move to row C. What works?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: seat = (row: 'C', number: seat.number); with seat declared var

    A record is immutable: every field has a getter and none has a setter. A changed value means a new record.

  3. Question 3 of 12 Which changes to the record type ({int total, int count}) produce a different type?

    Choose every answer that is right.

    Show the answer: Question 3 of 12

    Answer:

    • Renaming count to n
    • Adding a third field, double mean

    The shape is the number of positional fields, the names of the named fields and every field's type. Renames of named fields and added fields change it; order and aliases do not.

  4. Question 4 of 12 A function returns (int paise, int quantity), and a caller writes (int quantity, int paise) line = f();. What happens?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It compiles, and line.$1 is still the price in paise

    Positional names never reach the type, so a mislabelled annotation compiles and reads the same values. Named fields make every read say which field it means.

  5. Question 5 of 12 a = ('Pune', 411001) and b = ('Pune', 411000 + 1). What did equality.dart print for a.hashCode == b.hashCode?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: true

    Records define == and hashCode together from their fields, so equal records have equal hash codes.

  6. Question 6 of 12 order = ('Thali', tags) is made, and then tags.add('no onion') runs. What does print(order) show?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: (Thali, [veg, jain, no onion])

    A record is immutable one level deep. Its fields always hold the same objects, and those objects may change.

  7. Question 7 of 12 Why can a record of a String and an int serve as a map key?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: Equal records have equal hash codes and compare equal, which is what a hash table needs

    A map literal is a LinkedHashMap, a hash table whose keys need agreeing == and hashCode. Records of strings and ints have both.

  8. Question 8 of 12 A set is built from five seats: B1, B2, B1, C1 and B2, each as (row: ..., seat: ...). How many elements does it hold?

    Type a number.

    Show the answer: Question 8 of 12

    Answer: 3 elements

    B1 and B2 each appear twice, and equal records count once in a set: B1, B2 and C1 remain.

  9. Question 9 of 12 Which return type lets a caller read the results as range.min and range.max?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: ({int min, int max})

    Named fields in the return type become getters at the call site, which also keeps two ints from being mixed up.

  10. Question 10 of 12 What does a record return type check that List<int> does not?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: The number of values and the type of each one

    A record type fixes how many values there are and what type each has, so the compiler can check every use.

  11. Question 11 of 12 A private helper returns a count and an average to one caller in the same file. What fits best?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: A record with named fields

    For values that only travel together inside your code, a record is the lightest choice that the compiler still checks.

  12. Question 12 of 12 What does a typedef of a record type not give a public API, according to the records page?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Abstraction and encapsulation of how the data is stored

    A typedef is a second name for a shape, so every user still depends on that shape. A class can hide how its data is stored.

Interview questions

Warm-up (fresher): Two records hold the same values. When does == call them equal, and why does that make them safe map keys?

A record compares by contents, not by object. == first checks that the other value is a record of the same shape, the same number of positional fields and the same named fields, and then compares the matching fields with their own ==. The order in which named fields were written does not matter. Records also produce a hash code from their fields, so two equal records always hash alike, and a map or a set needs exactly that to find a key again: a record built later with the same row and seat finds the stored booking. The answer is only as good as the fields’ own equality, though. A field holding a List compares by identity, so two records with equal but separate lists are not equal, and such a record finds nothing when used as a key.

Two longer questions from the module’s bank build on this lesson:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A package exposes typedef Stats = ({int total, int count}) and a function that returns it. A new rule says count may never be negative, so Stats has to become a class. How do you make the change, and which code in your callers still breaks?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A memoised function keeps its last key and result and checks identical(key, lastKey) before it looks in its Map<(int, int), int> cache. Profiling shows the fast path never hits, although callers repeat the same (row, col) pair. Why, and what would you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How do you read the fields of a Dart record?

    Positional fields through the getters $1, $2 and so on, named fields by their names. A record is one immutable value holding a fixed set of fields.

  2. Why does final wrapped = (amount); not make a record?

    Parentheses alone do not make a record: wrapped gets amount's type, and wrapped.$1 does not compile. Write (amount,) with a trailing comma, or name the field: (amount: amount).

  3. What makes up a record's type?

    Its shape: the number of positional fields, the names of the named fields and the type of every field. Names written on positional fields are documentation, and a typedef only gives a shape a second name.

  4. When are two records equal?

    When their shapes match and every field is equal, each compared with its own ==; the order named fields were written in does not matter. A field holding a List therefore compares by identity.

  5. Does final order = ('Thali', tags); copy the tags list?

    No. The record holds the same list, so a later tags.add(...) shows up in order. Build the record from a copy when it must keep the values it had.

  6. How can one Dart function return two typed values without a class?

    With a record return type such as (int, int) or ({int min, int max}).

  7. When does a class or a map fit better than a record?

    A class when the data needs code or a long-lived public type, a map when the names come from the data itself.

Key takeaways

  • A record bundles a fixed set of positional and named fields into one immutable value; positional fields are read as $1, $2 and so on, named ones by their names, and a record with one positional field needs a trailing comma.
  • A record’s type is its shape: the number of positional fields, the names of named fields and every field’s type, while names written on positional fields are documentation and a typedef only gives a shape a second name.
  • Two records count as equal when their shapes match and every field is equal, each compared with its own ==, so a field holding a List compares by identity and can still change inside the record.
  • Records give matching == and hashCode, which makes records of strings and numbers dependable map keys and set elements, as the seat map showed.
  • A record return type such as (int, int) or ({int min, int max}) returns several typed values without a class; choose a class when the data needs code or a long-lived public type, and a map when names come from the data.

Terms in this lesson

named field
A field of a record written as name: value and read through a getter of the same name. Its name is part of the record's type, and the order of named fields is not.
positional field
A field of a record written without a name and read by its position, as $1, $2 and so on, counting only the positional fields from the left.
record
An anonymous, immutable value that bundles a fixed set of positional and named fields, each with its own type. Records need no declaration and compare equal when their shape and fields are equal.
record shape
The number of positional fields of a record, the names of its named fields and the type of every field. The shape alone decides a record's type.
structural equality
Equality decided by contents rather than by object: two records are equal when they have the same shape and every pair of corresponding fields is equal.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.