Dart のレコード・パターン・sealed クラスを実測で理解する

レコード、分解代入、switch 式、ガード、sealed クラスを Dart 3.11 で実行。網羅性エラーの実物、jsonDecode とマップパターンの JSON パース、AOT でのメモリと速度の実測付き。

シリーズ:Dart 講座 4/6(中級)。 ヘルパー関数から Map<String, dynamic> を返したり、if (x is ...) を何段も重ねたりしている Dart/Flutter 開発者向けです。読み終えると、レコードで複数の値を返し、パターンでデータを分解し、sealed クラスで画面の状態を表して抜けている case をコンパイラに見つけさせ、as キャストの壁なしで JSON をパースできるようになります。

レビューする Flutter のコードには、Dart 3 以前の癖が2つよく残っています。「2つの値を返す」ためだけの小さなクラスや Map と、isLoading・error・data を nullable で並べてありえない組み合わせまで許してしまう画面状態です。レコード・パターン・sealed クラスはこの両方を解決し、3つとも Dart 3.0 で一緒に入りました。この記事では Dart 3.11 で実際の動き、出会うエラーの実物、そして AOT ビルドで計ったコストを見ていきます。

実行環境: Dart SDK 3.11.5(stable、macos_arm64)、Apple Silicon Mac(M1 Pro)。例は dart run(JIT)で実行し、時間とメモリは dart compile exe(AOT)のバイナリで計りました。乱数は Random(42) で固定。アナライザのメッセージは dart analyze の出力です。

レコード:位置フィールド・名前付きフィールド・等価性

レコードは (int, String) や ({double lat, double lng}) のような型を持つ、名前のない不変の値の組です。座標の例は東京駅あたりにしました。

(int, String) parse(String line) {
  final i = line.indexOf('=');
  return (int.parse(line.substring(i + 1)), line.substring(0, i));
}

({double lat, double lng}) tokyo() => (lat: 35.68, lng: 139.76);

void main() {
  final r = parse('port=8080');
  print(r);
  print('${r.$1} ${r.$2}');
  final (value, key) = parse('timeout=30');   // 分解代入
  print('$key -> $value');

  final p = tokyo();
  print(p);
  final (:lat, :lng) = p;
  print('lat=$lat lng=$lng');

  print((1, 'a') == (1, 'a'));
  print((x: 1, y: 2) == (y: 2, x: 1));
  print((1, 2) == (2, 1));
  print({(1, 'a'), (1, 'a')}.length);
  print((1, 'a').runtimeType);
  print(identical((1, 2), (1, 2)));

  var (a, b) = (1, 2);
  (a, b) = (b, a);  // 入れ替え
  print('a=$a b=$b');
  final pos = (1, 2, label: 'p');
  print('${pos.$1} ${pos.$2} ${pos.label}');
}
(8080, port)
8080 port
timeout -> 30
(lat: 35.68, lng: 139.76)
lat=35.68 lng=139.76
true
true
false
1
(int, String)
false
a=2 b=1
1 2 p

位置フィールドは $1・$2 で読みます(0 ではなく 1 から)。名前付きフィールドは名前で読み、並び順は関係ありません。(x: 1, y: 2) == (y: 2, x: 1) は true です。== と hashCode は中身で比べる構造的等価なので、レコードの Set は重複を正しく取り除きます。第 3 回の「== だけ override して hashCode を忘れる」バグは、レコードでは起こりえません。

レコードは const でない限り正規化されません。identical((1, 2), (1, 2)) は false、identical(const (1, 2), const (1, 2)) は true で、JIT でも AOT ビルドでも同じでした。レコードの比較は identical ではなく == で行います。

パターン:分解代入・switch 式・if-case

パターンは形を確かめながら変数に取り出します。final (value, key) = ... はもう見ました。本領は switch 式です。

String bucket(int code) => switch (code) {
      >= 200 && < 300 => 'ok',
      301 || 302 || 307 || 308 => 'redirect',
      >= 400 && < 500 => 'client error',
      _ => 'other',
    };

void main() {
  print([200, 302, 404, 500].map(bucket).toList());

  final Object v = [1, 2, 3];
  if (v case [int first, ...final rest] when rest.isNotEmpty) {
    print('first=$first rest=$rest');
  }
}
[ok, redirect, client error, other]
first=1 rest=[2, 3]
  • >= 200 && < 300 は関係パターンを論理積でつないだもの。
  • 301 || 302 は定数パターンの論理和。
  • if (v case ...) は switch 全体を書かずに1つのパターンだけ照合します。v は Object 型ですが、リストパターンが「先頭が int のリストか」を確かめつつ first と rest を取り出すので、キャストは1つも出てきません。
  • when はガードで、追加で true でなければならない普通の条件式です。

(リダイレクトのコードは switch の題材にちょうどよく、Dart の http が POST の 301 を追わない件はまさにこの4つの話です。)

sealed クラスと網羅的な switch

sealed クラスは直接のサブタイプが同じライブラリ内に固定されます。コンパイラが一覧を全部知っているので、switch が全部をカバーしているか検査できます。Flutter の画面状態でよくある形です。

sealed class LoadState {}
class Loading extends LoadState {}
class Loaded extends LoadState {
  final List<String> items;
  Loaded(this.items);
}
class Failed extends LoadState {
  final Object error;
  final int retries;
  Failed(this.error, {this.retries = 0});
}

String label(LoadState s) => switch (s) {
      Loading() => 'spinner',
      Loaded(items: []) => 'empty list',
      Loaded(:final items) when items.length > 100 => 'big list (${items.length})',
      Loaded(:final items) => '${items.length} items',
      Failed(retries: >= 3) => 'give up',
      Failed(:final error) => 'retry: $error',
    };

void main() {
  for (final s in [Loading(), Loaded([]), Loaded(List.filled(150, 'x')), Loaded(['a', 'b']),
      Failed('timeout'), Failed('timeout', retries: 3)]) {
    print('${s.runtimeType.toString().padRight(8)} -> ${label(s)}');
  }
}
Loading  -> spinner
Loaded   -> empty list
Loaded   -> big list (150)
Loaded   -> 2 items
Failed   -> retry: timeout
Failed   -> give up

Loaded(:final items) はオブジェクトパターンで、型を確かめつつ items ゲッターを同名の変数に取り出します。Loaded(items: []) は空リストのときだけ一致します。ウィジェットの build() では、同じ switch から CircularProgressIndicator・ListView・エラー表示を返すことになります。

流れ図。sealed クラス LoadState が switch 式に入る。分岐は3つで、Loading はスピナー、Loaded はリスト表示、Failed はエラー表示へ進む。新しく足したサブタイプ Refreshing からの点線の矢印は、コンパイルエラー non_exhaustive_switch_expression へ進む。
sealed の LoadState が1つの switch 式を動かす。コンパイラがすべてのサブタイプを知っているので、case のない新しいサブタイプを足すと、真っ白な画面ではなくコンパイルエラーになる。

ここからが本題です。チームの誰かが class Refreshing extends LoadState {} を追加し、UI の対応を忘れたとします。

error - nonex.dart:7:30 - The type 'LoadState' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Refreshing()'. Try adding a wildcard pattern or cases that match 'Refreshing()'. - non_exhaustive_switch_expression

dart run も同じく Error: The type 'LoadState' is not exhaustively matched by the switch cases since it doesn't match 'Refreshing()'. で止まります。抜けている case を名指ししてくれる。状態を bool の組み合わせではなく sealed クラスで表す理由はここにあります。

網羅性でよくある間違い

間違い 1:sealed ではなく abstract にする。 abstract class LoadState で同じ2つの case だけ書くと、switch 式のエラーは doesn't match the pattern 'LoadState()' に変わります。コンパイラはサブクラスの一覧を知らないので、ワイルドカードを求めるしかありません。さらに悪いことに、abstract クラスに対する switch 文は何の警告もなくコンパイルされ、扱っていないサブタイプは黙って素通りします。直し方: sealed にし、sealed 型には _ => を書かないことで、新しいサブタイプがビルドを止めるようにします。

間違い 2:ガードも網羅性に数えられると思う。

sealed class R {}
class Ok extends R { final int v; Ok(this.v); }
class Err extends R {}
String a(R r) => switch (r) {
      Ok(:final v) when v > 0 => 'positive',
      Ok(:final v) when v <= 0 => 'not positive',
      Err() => 'err',
    };
error - The type 'R' isn't exhaustively matched by the switch cases since it doesn't match the pattern 'Ok()'. … - non_exhaustive_switch_expression

人間には v > 0 と v <= 0 で全部だと分かりますが、コンパイラはガードの中身を推論しません。直し方: Ok の最後の case をガードなしの Ok() => 'not positive' にします。

間違い 3:絶対に実行されない case。 Ok() => 'ok' を Ok(v: 0) => 'zero' より前に置くと warning - This case is covered by the previous cases. … - unreachable_switch_case が出ます。具体的な case から先に並べます。

マップパターンと jsonDecode で JSON をパースする

Flutter アプリでパターンが一番効くのはここです。中身を完全には管理できない「アップデート確認 API」の JSON を読みます。

import 'dart:convert';

final class AppVersion {
  final String platform;
  final String latest;
  final int minBuild;
  final String? notes;
  const AppVersion(this.platform, this.latest, this.minBuild, this.notes);
  @override
  String toString() => 'AppVersion($platform $latest, min build $minBuild, notes: $notes)';
}

AppVersion? parseVersion(Object? json) {
  if (json case {
        'platform': final String platform && ('ios' || 'android'),
        'latest': final String latest,
        'min_build': final int minBuild,
      }) {
    final notes = switch (json['notes']) { final String s => s, _ => null };
    return AppVersion(platform, latest, minBuild, notes);
  }
  return null;
}

AppVersion parseVersionManual(Map<String, dynamic> json) => AppVersion(
      json['platform'] as String,
      json['latest'] as String,
      json['min_build'] as int,
      json['notes'] as String?,
    );

void main() {
  const bodies = [
    '{"platform":"ios","latest":"2.4.0","min_build":120,"notes":"bug fixes"}',
    '{"platform":"android","latest":"2.4.0","min_build":118}',
    '{"platform":"web","latest":"2.4.0","min_build":1}',
    '{"platform":"ios","latest":"2.4.0","min_build":"120"}',
    '{"platform":"ios","latest":"2.4.0","min_build":120.0}',
    '[1,2,3]',
  ];
  for (final body in bodies) {
    print(parseVersion(jsonDecode(body)));
  }
  print('--- manual casts on the 4th body:');
  try {
    parseVersionManual(jsonDecode(bodies[3]) as Map<String, dynamic>);
  } catch (e) {
    print('${e.runtimeType}: $e');
  }
  print('--- extra keys are fine for map patterns:');
  print(parseVersion(jsonDecode('{"platform":"ios","latest":"3.0.0","min_build":200,"force":true}')));
  final decoded = jsonDecode('{"a":1}');
  print('jsonDecode map type: ${decoded.runtimeType}');
}
AppVersion(ios 2.4.0, min build 120, notes: bug fixes)
AppVersion(android 2.4.0, min build 118, notes: null)
null
null
null
null
--- manual casts on the 4th body:
_TypeError: type 'String' is not a subtype of type 'int' in type cast
--- extra keys are fine for map patterns:
AppVersion(ios 3.0.0, min build 200, notes: null)
jsonDecode map type: _Map<String, dynamic>

1つのパターンが、例外を1つも投げずに4種類の不正な入力を弾きました。未知のプラットフォーム、文字列の "120"、double の 120.0、オブジェクトでなく配列。意外なのは 120.0 で、jsonDecode は小数点付きの数値を double で返すため final int に一致しません。キャスト版は _TypeError で落ちました。Flutter アプリならデバッグでは赤い画面、リリースでは壊れた画面です。サーバー側の数値の型は実際に変わります。プリペアドステートメントで JSON の型が変わった PHP の記事では、"7" が 7 になって古い Flutter ビルドが落ちた実例を書きました。

マップパターンは余分なキーを無視します("force": true があっても通った)。サーバーがフィールドを増やしても古いアプリは壊れません。

最初にやった間違い。 最初の版ではプラットフォームの確認を 'platform': 'ios' || 'android' && final String platform と書き、コンパイルが通りませんでした。

error - json.dart:16:21 - Variable pattern 'platform' is missing in this branch of the logical-or pattern. Try declaring this variable pattern in the branch. - missing_variable_pattern

&& は || より強く結合するので、これは 'ios' || ('android' && final String platform) と読まれ、'ios' 側の分岐では変数が束縛されません。直し方: 先に束縛してから確かめ、括弧を付けます。final String platform && ('ios' || 'android')。

実測:レコード対小さなクラス、パターン対キャスト(AOT)

すべて AOT ビルド(dart compile exe)で、ウォームアップ3回のあと9回計った中央値、データは Random(42) です。

1. int を2つ返す関数を 1,000 万回。 mmRecord は (int, int) を、mmClass は final class MinMax を返します。@pragma('vm:never-inline') 付き(本物の関数呼び出し)と、なし(インライン化されうる)の両方で計りました。

record, never-inline (10M)         23.1 ms
class, never-inline (10M)          43.1 ms
record, inlinable (10M)            13.2 ms
class, inlinable (10M)             13.2 ms
json: map pattern (200k x5)        45.6 ms
json: as casts (200k x5)           41.2 ms

2回目は 23.4 / 47.4 / 13.3 / 13.4 / 47.2 / 42.6ms でした。インライン化されると両者は同じで、どちらも割り当てが消えています。本物の呼び出しを挟むと、AOT ではレコードが約2倍速くなりました。JIT(dart run)ではその差が消えました(69.1 対 66.7ms)。AOT コンパイラがレコードを割り当てずにフィールドを返していると考えると辻褄が合いますが、機械語は確認していないので「おそらく」の説明として読んでください。

棒グラフ。インライン化を禁止した呼び出しでは、レコードを返すと 23.1 ミリ秒、小さなクラスを返すと 43.1 ミリ秒。インライン化を許すとどちらも 13.2 ミリ秒。
関数から int を2つ返す処理を 1,000 万回。ウォームアップ3回のあと計測9回の中央値(Dart 3.11.5 AOT、Apple Silicon Mac M1 Pro)。インライン化されるとどちらも 13.2ms で、差は実際の関数呼び出しのときだけ出る。

2. レコードを保持したときのメモリ。 100 万個を List に入れ、新しいプロセスで RSS の増加を見ました。基準は int だけのリストです(プログラム全体は「自分で動かす」に)。

baseline  1M: +7.6 MiB  (1000000)
record    1M: +40.9 MiB  (1000000)
named     1M: +41.0 MiB  (1000000)
class     1M: +31.0 MiB  (1000000)
record-d  1M: +77.5 MiB  (1000000)
class-d   1M: +31.0 MiB  (1000000)

record は (i, i + 1)、named は (min: i, max: i + 1)、class は final class MinMax { final int min, max; }、record-d は (i * 0.5, i + 0.5)、class-d は final class Pt { final double x, y; } です。2回目も 0.7MiB 以内で一致しました。保持するとレコードは同等のクラスより大きく、double では倍以上になりました。クラスは final double フィールドをボックス化せずに持ちますが、レコードのフィールドは参照を持つので、double が1つずつヒープ上のオブジェクトになります。100 万個の座標を持つならクラスにします。

100 万個を保持したときのメモリ増加の棒グラフ。int のリストは 7.6 MiB、int フィールド2つのクラスは 31.0、(int, int) レコードは 40.9、double フィールド2つのクラスは 31.0、(double, double) レコードは 77.5。
100 万個の値を List に保持したときの RSS 増加(Dart 3.11.5 AOT、Apple Silicon Mac M1 Pro)。int だけのリストで 7.6MiB。レコードのフィールドは double をボックス化して持ち、クラスのフィールドはボックス化しない。

3. マップパターン対 as キャスト。 デコード済みの JSON オブジェクト 20 万個を5回パース(計 100 万回)。パターンは 45.6ms、キャストは 41.2ms で約 10% 遅く、JIT では 42.7 対 41.0ms でした。マップパターンはキーの存在を確かめてから読むので、その分の仕事が増えていると考えられます。1オブジェクトあたり約 45ns の差です。API のレスポンスは普通1回しかパースしないので、不正な入力に強いほうを選びましょう。

自分で動かす

どちらも SDK だけで動きます。保存して dart compile exe bench.dart -o bench && ./bench、dart compile exe mem.dart -o mem && ./mem record を実行します(モードは baseline・record・named・class・record-d・class-d)。

import 'dart:convert';
import 'dart:math';

final class MinMax {
  final int min, max;
  const MinMax(this.min, this.max);
}

@pragma('vm:never-inline')
(int, int) mmRecord(List<int> xs, int i) => (xs[i], xs[i + 1]);
@pragma('vm:never-inline')
MinMax mmClass(List<int> xs, int i) => MinMax(xs[i], xs[i + 1]);

(int, int) mmRecordInl(List<int> xs, int i) => (xs[i], xs[i + 1]);
MinMax mmClassInl(List<int> xs, int i) => MinMax(xs[i], xs[i + 1]);

final class Item {
  final String id;
  final int qty;
  final String? note;
  Item(this.id, this.qty, this.note);
}

Item? parsePattern(Object? j) {
  if (j case {'id': final String id, 'qty': final int qty}) {
    return Item(id, qty, j['note'] as String?);
  }
  return null;
}

Item parseCast(Map<String, dynamic> j) =>
    Item(j['id'] as String, j['qty'] as int, j['note'] as String?);

int sink = 0;

double median(void Function() f, {int warm = 3, int runs = 9}) {
  for (var i = 0; i < warm; i++) f();
  final t = <double>[];
  for (var i = 0; i < runs; i++) {
    final sw = Stopwatch()..start();
    f();
    t.add(sw.elapsedMicroseconds / 1000);
  }
  t.sort();
  return t[runs ~/ 2];
}

void main() {
  final rnd = Random(42);
  const n = 10000000;
  final xs = List<int>.generate(n + 1, (_) => rnd.nextInt(1000));
  void report(String name, double ms) => print('${name.padRight(34)} ${ms.toStringAsFixed(1)} ms');

  report('record, never-inline (10M)', median(() {
    var s = 0;
    for (var i = 0; i < n; i++) { final (a, b) = mmRecord(xs, i); s += a - b; }
    sink += s;
  }));
  report('class, never-inline (10M)', median(() {
    var s = 0;
    for (var i = 0; i < n; i++) { final m = mmClass(xs, i); s += m.min - m.max; }
    sink += s;
  }));
  report('record, inlinable (10M)', median(() {
    var s = 0;
    for (var i = 0; i < n; i++) { final (a, b) = mmRecordInl(xs, i); s += a - b; }
    sink += s;
  }));
  report('class, inlinable (10M)', median(() {
    var s = 0;
    for (var i = 0; i < n; i++) { final m = mmClassInl(xs, i); s += m.min - m.max; }
    sink += s;
  }));

  final rows = List.generate(200000, (i) => {'id': 'sku-$i', 'qty': rnd.nextInt(50), if (i.isEven) 'note': 'n$i'});
  final decoded = (jsonDecode(jsonEncode(rows)) as List).cast<Map<String, dynamic>>();
  report('json: map pattern (200k x5)', median(() {
    var s = 0;
    for (var k = 0; k < 5; k++) for (final r in decoded) { s += parsePattern(r)!.qty; }
    sink += s;
  }));
  report('json: as casts (200k x5)', median(() {
    var s = 0;
    for (var k = 0; k < 5; k++) for (final r in decoded) { s += parseCast(r).qty; }
    sink += s;
  }));
  print('sink ${sink != 0}');
}
import 'dart:io';
final class MinMax { final int min, max; const MinMax(this.min, this.max); }
final class Pt { final double x, y; Pt(this.x, this.y); }
void main(List<String> a) {
  const n = 1000000;
  final before = ProcessInfo.currentRss;
  final List<Object> l = switch (a[0]) {
    'record' => List.generate(n, (i) => (i, i + 1)),
    'class' => List.generate(n, (i) => MinMax(i, i + 1)),
    'record-d' => List.generate(n, (i) => (i * 0.5, i + 0.5)),
    'class-d' => List.generate(n, (i) => Pt(i * 0.5, i + 0.5)),
    'named' => List.generate(n, (i) => (min: i, max: i + 1)),
    _ => List.generate(n, (i) => i),
  };
  final after = ProcessInfo.currentRss;
  print('${a[0].padRight(9)} 1M: +${((after - before) / 1048576).toStringAsFixed(1)} MiB  (${l.length})');
}

判断表

やりたいこと 使うもの 理由
private なヘルパーから2〜3個の値を返す レコード (a, b) クラス名が要らず、== も正しい
複数ファイルで使う値型、メソッド付き クラス(または final class) 名前・ドキュメント・メソッド
数値を何百万個もメモリに持つ final double フィールドのクラス レコードは double をボックス化(77.5 対 31.0MiB)
決まった種類の状態や結果 sealed class + switch 式 抜けた case をコンパイラが見つける
決まった種類の単純な定数 enum switch で網羅性検査が効く
信用できない JSON if-case のマップパターン 例外を投げずに型違いを弾く
信用できる、生成された JSON モデル キャストかコード生成の fromJson わずかに速く、コードが短い
複数フィールドからなる Set/Map のキー レコード 構造的な hashCode が付いてくる

正直な限界とバージョンのメモ

「Dart 3.0 以降」という説明は、レコード型・sealed・分解代入を使うファイルの先頭に // @dart=2.19 を付けて確かめました。

error - old219.dart:2:1 - This requires the 'records' language feature to be enabled. Try updating your pubspec.yaml to set the minimum SDK constraint to 3.0.0 or higher, and running 'pub get'. - experiment_not_enabled
error - old219.dart:3:1 - This requires the 'sealed-class' language feature to be enabled. … - experiment_not_enabled
error - old219.dart:5:9 - Expected an identifier. - missing_identifier
error - old219.dart:5:9 - Expected to find ';'. - expected_token
…
21 issues found.

レコードと sealed には分かりやすい「機能」のメッセージが出ますが、パターンには出ません。2.19 では final (a, b) = pair(); がそもそも構文として読めず、missing_identifier/expected_token が大量に出ます。Dart 3 として正しく見えるコードでこういうエラーが出たら、まず pubspec.yaml の SDK 制約を確認してください。// @dart=3.0 にすると同じファイルが 1 a one と出力しました。

  • 実行は Dart 3.11.5 だけで、3.0 の SDK はインストールしていません。
  • メモリの数値は RSS の増加で、GC ヒープの余裕分を含みます。2回の実行で安定していましたが、相対比較として見てください。
  • never-inline の計測は、実際のコードにはないかもしれない呼び出しを強制しています。インライン化されればレコードとクラスは同じでした。
  • この回では Flutter アプリは動かしていません。Flutter の部分は、ウィジェットの build() から呼ぶ普通の Dart コードです。

練習問題

問題 1. sealed class Shape と Circle(double r)・Rect(double w, double h) に対して、ワイルドカードなしの switch 式で double area(Shape s) を書いてください。そのあと Square(double side) を追加してエラーを読んでください。

問題 2. {"user": {"id": 7, "tags": ["a", "b"]}} を if-case 1つでレコード (int id, List<String> tags) にしてください。tags がない、または文字列以外を含む場合は null を返します。

解答

1.

sealed class Shape {}
class Circle extends Shape { final double r; Circle(this.r); }
class Rect extends Shape { final double w, h; Rect(this.w, this.h); }

double area(Shape s) => switch (s) {
      Circle(:final r) => 3.141592653589793 * r * r,
      Rect(:final w, :final h) => w * h,
    };

class Square extends Shape { ... } を足すと non_exhaustive_switch_expression ... doesn't match the pattern 'Square()' になりました。

2.

(int, List<String>)? parseUser(Object? json) {
  if (json case {'user': {'id': final int id, 'tags': final List<Object?> raw}}
      when raw.every((t) => t is String)) {
    return (id, raw.cast<String>());
  }
  return null;
}

jsonDecode が作るのは List<dynamic> なので、List<String> のパターンは決して一致しません(実際に試すと null でした)。要素はガードで確かめます。上の入力では (7, [a, b]) と出力されました。


関連記事:Java のクラス・レコード・インターフェース · Swift のエラー処理とジェネリクス · PHP の enum とインターフェース

前回: 第 3 回 クラス・mixin・拡張メソッド · 次回: 第 5 回 非同期・Future・Stream・Isolate

よくある質問

Dart のレコードは小さなクラスより速い・小さいのですか?

場合によります。インライン化しない関数から2値を返す処理 1,000 万回は、レコードで 23.1ms、クラスで 43.1ms でした(AOT、Apple Silicon Mac M1 Pro)。一方リストに 100 万個保持すると、(int, int) レコードは RSS +40.9MiB、クラスは +31.0MiB、(double, double) レコードは double がボックス化されて +77.5MiB でした。

sealed クラスに新しいサブクラスを足すと何が起きますか?

その型を扱う switch 式のうち新しい case のないものがすべて non_exhaustive_switch_expression でコンパイルできなくなり、"doesn't match the pattern 'Refreshing()'" のように抜けている case を名指しします。ただの abstract クラスでは「ワイルドカードを足せ」としか言われず、switch 文は警告なしで通ります。

レコードやパターンは Dart 3 が必要ですか?

必要です。// @dart=2.19 を付けると、レコードと sealed には experiment_not_enabled が出て、final (a, b) = pair(); のような分解代入は missing_identifier や expected_token の構文エラーの山になります。// @dart=3.0 なら同じファイルが動きます。

  • #Dart
  • #Flutter
  • #レコード
  • #パターンマッチング
  • #sealed クラス
  • #JSON
  • #Dart 3
  • #中級

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑