Dart 3 新機能 — Records / パターンマッチング / シールクラス
Dart 3 の三大機能 — コードを「動く」から「エレガント」にアップグレードする。
1. 学べること
- Records:軽量な匿名データ集約、複数の戻り値
- パターンマッチング:分解 / if-case / switch パターン / 網羅性チェック
- シールクラス:制限された継承階層 + 網羅的 switch 保証
- 三矢の活用:シール + パターン + Records の組み合わせ
- Bob のシナリオ:DataPipeline でシールクラスを使ったデータソース型定義、パターンマッチングを使った結果解析
2. 開発者のリアルな物語
(1) 課題:深い型階層と冗長な if-else
Bob の DataPipeline には 3 つのデータソース(API/File/Database)があり、それぞれ設定と接続方法が異なる。彼は継承を使って実装したが、階層が深くコードが冗長だった。API レスポンスを解析する際、ステータスコードとレスポンス構造に対する多数の if-else チェックが必要で、たった 5 つのレスポンスタイプに対して 200 行の条件付きコードを書いた。
(2) Dart 3 新機能による解決
シールクラスはデータソース型を制限し、パターンマッチングはさまざまなレスポンスをエレガントに処理し、Records は軽量な複数の戻り値を提供する。
// シールクラス: 網羅的なデータソース型
sealed class DataSource {}
class ApiSource extends DataSource { final String endpoint; ApiSource(this.endpoint); }
class FileSource extends DataSource { final String path; FileSource(this.path); }
// シールクラスのパターンマッチング - コンパイラが全ケース処理を保証
String describe(DataSource source) => switch (source) {
ApiSource(:final endpoint) => 'API: $endpoint',
FileSource(:final path) => 'File: $path',
};
// Records: 複数の戻り値
(String, double) parseResponse(String raw) => ('ORD-001', 1500.0);
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- シールクラスにより型の分岐が有限で列挙可能になり、網羅的 switch が漏れゼロを保証する。
- パターンマッチングにより冗長な if-else チェーンが宣言的な switch 式に置き換わる。
- Records が小さなクラスを置き換え、複数の戻り値に Tuple ライブラリや Map が不要になる。
3. Records
(1) Record の基礎
▶ サンプル:Record の作成と使用
void main() {
// 位置指定 Record
var order = ('ORD-001', 1500.0, 'completed');
print(order.$1); // ORD-001
print(order.$2); // 1500.0
print(order.$3); // completed
// 名前付き Record(可読性のため推奨)
var order2 = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print(order2.id); // ORD-002
print(order2.amount); // 3200.0
print(order2.status); // pending
// 位置指定と名前付きの混在
var mixed = ('ORD-003', 890.0, category: 'Electronics');
print(mixed.$1); // ORD-003
print(mixed.category); // Electronics
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:関数戻り値としての Record
// Record を使った複数の戻り値
({String id, double amount, double tax}) calculateOrder(double amount, double taxRate) {
return (
id: 'ORD-${DateTime.now().millisecondsSinceEpoch}',
amount: amount,
tax: amount * taxRate,
);
}
// 簡易グループ化のための位置指定 Record
(String, double) parseAmount(String input) {
final parts = input.split(':');
return (parts[0], double.parse(parts[1]));
}
void main() {
final order = calculateOrder(1500.0, 0.08);
print('ID: ${order.id}, Amount: \$${order.amount}, Tax: \$${order.tax}');
final (label, value) = parseAmount('Revenue:52500.75');
print('$label: \$$value USD');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| Record 機能 | 構文 | 説明 |
|---|---|---|
| 位置指定フィールド | .$1, .$2 |
位置でアクセス |
| 名前付きフィールド | .name |
名前でアクセス |
| 型アノテーション | (String, int) または ({String name, int count}) |
明示的な型 |
| 等価性 | 値の等価性 | フィールド値が同じなら等しい |
4. パターンマッチング
(1) パターンマッチングの種類
▶ サンプル:変数パターンと分解
void main() {
// 変数パターン - 値を抽出
var (id, amount, status) = ('ORD-001', 1500.0, 'completed');
print('ID: $id, Amount: $amount, Status: $status');
// 名前付きフィールドでの Record 分解
var (:id, :amount, :status) = (id: 'ORD-002', amount: 3200.0, status: 'pending');
print('ID: $id, Amount: $amount, Status: $status');
// List の分解
var [first, second, ...rest] = [1, 2, 3, 4, 5];
print('First: $first, Second: $second, Rest: $rest');
// Map の分解
var {'id': orderId, 'amount': orderAmount} = {'id': 'ORD-003', 'amount': 890.0};
print('Order: $orderId, Amount: $orderAmount');
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:if-case パターン
void main() {
Object value = '1,500.00 USD';
// if-case パターンマッチング
if (value case String s when s.contains('USD')) {
print('USD value: $s');
}
// Record に対するパターンマッチング
var response = (statusCode: 200, body: '{"orders": 1500}');
if (response case (statusCode: 200, :var body)) {
print('Success response: $body');
}
// List に対するパターンマッチング
var orders = ['ORD-001', 'ORD-002', 'ORD-003'];
if (orders case [var first, var second, ...]) {
print('First two: $first, $second');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:switch パターン
String classifyOrder(Object value) => switch (value) {
// 型パターン
int i when i > 1000 => 'High value integer: $i',
int i => 'Low value integer: $i',
double d when d >= 1000 => 'Premium: \$${d.toStringAsFixed(2)}',
double d => 'Standard: \$${d.toStringAsFixed(2)}',
String s => 'String: $s',
List l when l.length > 100 => 'Large batch: ${l.length} items',
List l => 'Small batch: ${l.length} items',
_ => 'Unknown type',
};
// Record でのパターンマッチング
String describeResponse((int, String) response) => switch (response) {
(200, var body) => 'OK: $body',
(404, _) => 'Not Found',
(500, var msg) => 'Server Error: $msg',
(>= 400, var msg) => 'Client Error: $msg',
_ => 'Unknown response',
};
void main() {
print(classifyOrder(1500)); // High value integer: 1500
print(classifyOrder(890.0)); // Standard: $890.00
print(classifyOrder([1, 2, 3])); // Small batch: 3 items
print(describeResponse((200, 'OK'))); // OK: OK
print(describeResponse((404, 'Missing'))); // Not Found
print(describeResponse((500, 'Crash'))); // Server Error: Crash
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| パターンタイプ | 構文 | 用途 |
|---|---|---|
| 変数パターン | var x |
値を抽出 |
| 型パターン | Type x |
型チェック + 抽出 |
| 定数パターン | 42、'hello' |
完全一致 |
| 関係パターン | >= 1000 |
範囲チェック |
| 論理パターン | a || b、a && b |
条件の組み合わせ |
| ガード | when condition |
追加条件 |
| ワイルドカード | _ |
値を無視 |
5. シールクラス
(1) シールクラス階層
classDiagram
class DataSource {
<<sealed>>
}
class ApiSource {
+String endpoint
+Map headers
}
class FileSource {
+String path
+Encoding encoding
}
class DatabaseSource {
+String connectionString
}
DataSource <|-- ApiSource
DataSource <|-- FileSource
DataSource <|-- DatabaseSource
note for DataSource "網羅的 switch 保証"
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:シールクラス定義と網羅的マッチング
sealed class DataSource {
const DataSource();
}
class ApiSource extends DataSource {
final String endpoint;
final Map<String, String> headers;
const ApiSource(this.endpoint, {this.headers = const {}});
}
class FileSource extends DataSource {
final String path;
const FileSource(this.path);
}
class DatabaseSource extends DataSource {
final String connectionString;
const DatabaseSource(this.connectionString);
}
// 網羅的 switch - コンパイラが全サブタイプをチェック
String describeSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final headers) =>
'API: $endpoint (${headers.length} headers)',
FileSource(:final path) =>
'File: $path',
DatabaseSource(:final connectionString) =>
'Database: $connectionString',
};
// 新しいサブタイプを追加して switch の更新を忘れた場合、
// コンパイラがエラーを報告する!
void main() {
final sources = <DataSource>[
ApiSource('https://api.example.com/orders', headers: {'Authorization': 'Bearer token'}),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
for (final source in sources) {
print(describeSource(source));
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| シールクラス機能 | 説明 |
|---|---|
| サブクラス制限 | 同じライブラリ内で定義する必要がある |
| 網羅性チェック | switch が全サブタイプをカバーする必要がある |
| コンパイル時保証 | 新しいサブクラス追加時の switch 漏れはコンパイルエラーになる |
| インスタンス化不可 | 暗黙的に abstract |
6. 三矢の活用
▶ サンプル:シール + パターン + Records の組み合わせ
// API 結果用のシールクラス
sealed class ApiResult<T> {
const ApiResult();
}
class Success<T> extends ApiResult<T> {
final T data;
final (int, String) metadata; // Record: (statusCode, message)
const Success(this.data, this.metadata);
}
class ApiError<T> extends ApiResult<T> {
final String message;
final int? statusCode;
const ApiError(this.message, {this.statusCode});
}
class NetworkError<T> extends ApiResult<T> {
final String reason;
const NetworkError(this.reason);
}
// 分解付きのパターンマッチング
String handleResult(ApiResult<List<String>> result) => switch (result) {
Success(:final data, metadata: (200, final msg)) =>
'OK ($msg): ${data.length} items loaded',
Success(:final data, metadata: (final code, _)) =>
'Loaded with status $code: ${data.length} items',
ApiError(:final message, statusCode: final code?) =>
'API Error [$code]: $message',
ApiError(:final message) =>
'API Error: $message',
NetworkError(:final reason) =>
'Network Error: $reason',
};
void main() {
final results = <ApiResult<List<String>>>[
Success(['ORD-001', 'ORD-002'], (200, 'OK')),
Success(['ORD-003'], (206, 'Partial Content')),
ApiError('Rate limit exceeded', statusCode: 429),
ApiError('Unknown error'),
NetworkError('Connection timeout'),
];
for (final result in results) {
print(handleResult(result));
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
7. Bob のシナリオ:DataPipeline のデータソースと結果解析
▶ サンプル:完全なデータソース抽象化
sealed class DataSource {
const DataSource();
String get displayName;
}
class ApiSource extends DataSource {
final String endpoint;
final Duration timeout;
const ApiSource(this.endpoint, {this.timeout = const Duration(seconds: 10)});
@override
String get displayName => 'API ($endpoint)';
}
class FileSource extends DataSource {
final String path;
final String format;
const FileSource(this.path, {this.format = 'csv'});
@override
String get displayName => 'File ($path, $format)';
}
class DatabaseSource extends DataSource {
final String connectionString;
final String query;
const DatabaseSource(this.connectionString, {this.query = 'SELECT * FROM orders'});
@override
String get displayName => 'Database ($connectionString)';
}
// Record を使った処理結果
typedef ProcessingResult = ({int processed, int skipped, double revenue, Duration time});
ProcessingResult processDataSource(DataSource source) => switch (source) {
ApiSource(:final endpoint, :final timeout) => (
processed: 50000,
skipped: 120,
revenue: 525000.0,
time: timeout,
),
FileSource(:final path, :final format) => (
processed: 100000,
skipped: 350,
revenue: 1200000.0,
time: Duration(seconds: 5),
),
DatabaseSource(:final connectionString, :final query) => (
processed: 1200000,
skipped: 800,
revenue: 15000000.0,
time: Duration(seconds: 15),
),
};
void main() {
final sources = <DataSource>[
ApiSource('https://api.example.com/orders'),
FileSource('/data/orders.csv'),
DatabaseSource('postgresql://localhost:5432/orders'),
];
print('=== DataPipeline Source Analysis ===');
for (final source in sources) {
final (:processed, :skipped, :revenue, :time) = processDataSource(source);
print('\n${source.displayName}:');
print(' Processed: ${processed} orders');
print(' Skipped: $skipped records');
print(' Revenue: \$${revenue.toStringAsFixed(2)} USD');
print(' Time: ${time.inSeconds}s');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
8. 完全なサンプル:DataPipeline レスポンス処理システム
// ============================================
// DataPipeline レスポンス処理システム
// シールクラス + パターンマッチング + Records
// ============================================
// 異なるレスポンスタイプ用のシールクラス
sealed class ApiResponse {
const ApiResponse();
}
class SuccessResponse extends ApiResponse {
final int statusCode;
final Map<String, dynamic> data;
const SuccessResponse(this.statusCode, this.data);
}
class ErrorResponse extends ApiResponse {
final int statusCode;
final String message;
final String? details;
const ErrorResponse(this.statusCode, this.message, {this.details});
}
class TimeoutResponse extends ApiResponse {
final Duration timeout;
final String endpoint;
const TimeoutResponse(this.timeout, this.endpoint);
}
class RedirectResponse extends ApiResponse {
final String newEndpoint;
final int statusCode;
const RedirectResponse(this.newEndpoint, this.statusCode);
}
// Record としての処理結果
typedef Outcome = ({bool success, String message, double? revenue});
// 全レスポンスタイプに対するパターンマッチング - 網羅的!
Outcome handleResponse(ApiResponse response) => switch (response) {
SuccessResponse(statusCode: 200, :final data) when data.containsKey('orders') => (
success: true,
message: 'Loaded ${data['orders']} orders',
revenue: (data['revenue'] as num?)?.toDouble(),
),
SuccessResponse(statusCode: 200, :final data) => (
success: true,
message: 'OK but no orders field: ${data.keys}',
revenue: null,
),
SuccessResponse(statusCode: 206, :final data) => (
success: true,
message: 'Partial data: ${data.length} fields',
revenue: null,
),
SuccessResponse(statusCode: final code) => (
success: true,
message: 'Unexpected success code: $code',
revenue: null,
),
ErrorResponse(statusCode: 429, :final message) => (
success: false,
message: 'Rate limited: $message',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message, :final details?) => (
success: false,
message: 'Error [$code]: $message - $details',
revenue: null,
),
ErrorResponse(statusCode: final code, :final message) => (
success: false,
message: 'Error [$code]: $message',
revenue: null,
),
TimeoutResponse(:final timeout, :final endpoint) => (
success: false,
message: 'Timeout after ${timeout.inSeconds}s on $endpoint',
revenue: null,
),
RedirectResponse(:final newEndpoint, :final statusCode) => (
success: false,
message: 'Redirect ($statusCode) to $newEndpoint',
revenue: null,
),
};
void main() {
final responses = <ApiResponse>[
SuccessResponse(200, {'orders': 50000, 'revenue': 525000.0}),
SuccessResponse(200, {'products': 3000}),
SuccessResponse(206, {'partial': true}),
ErrorResponse(429, 'Rate limit exceeded'),
ErrorResponse(500, 'Internal server error', details: 'Database connection lost'),
ErrorResponse(404, 'Not found'),
TimeoutResponse(const Duration(seconds: 30), '/api/v1/orders'),
RedirectResponse('/api/v2/orders', 301),
];
print('=== DataPipeline Response Handler ===\n');
for (final response in responses) {
final (:success, :message, :revenue) = handleResponse(response);
final status = success ? 'OK' : 'FAIL';
final rev = revenue != null ? ' (\$${revenue.toStringAsFixed(2)} USD)' : '';
print('[$status] $message$rev');
}
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Response Handler ===
[OK] Loaded 50000 orders ($525000.00 USD)
[OK] OK but no orders field: (products)
[OK] Partial data: 1 fields
[FAIL] Rate limited: Rate limit exceeded
[FAIL] Error [500]: Internal server error - Database connection lost
[FAIL] Error [404]: Not found
[FAIL] Timeout after 30s on /api/v1/orders
[FAIL] Redirect (301) to /api/v2/orders
❓ よくある質問
Q: Records と Classes の違いは何ですか? A: Records は匿名の値ベースの軽量データ構造で、Classes は名前があり、参照ベースで、メソッドを持てます。Records は単純なデータ渡しに適し、Classes は複雑なビジネスロジック用です。
Q: シールクラスと抽象クラスの違いは何ですか? A: シールクラスはサブクラスを同じライブラリに制限し、コンパイラが網羅性チェックを実行できるようにします。抽象クラスはどこでもサブクラスを定義でき、網羅性が妨げられます。網羅性保証が必要なときはシールを使ってください。
Q: パターンマッチングはどこで使えますか? A: switch 式、switch 文、if-case、変数宣言、for-in ループ。Dart 3 ではパターンはどこにでもあります。
Q: Records のパフォーマンスはどうですか? A: Records は通常のオブジェクトにコンパイルされ、パフォーマンスは小さなクラスと同等です。余分なオーバーヘッドはありませんが、クラスより速いわけでもありません。可読性に基づいて選んでください。
Q: シールクラスはコンストラクタを持てますか? A: ファクトリコンストラクタ(サブクラスを返す)は持てますが、クラス自体はインスタンス化できません(暗黙的に abstract)。コンストラクタは主にサブクラス間での初期化ロジックの共有に使われます。
Q: switch 式での
_とdefaultの違いはありますか? A: 機能的には同じで、どちらも全キャッチマッチです。Dart 3 では簡潔さのため_(ワイルドカードパターン)の使用が推奨されます。defaultは互換性のために残されているレガシー構文です。
Q: Records を Map のキーに使えますか? A: はい。Records は値の等価性に基づき、
==とhashCodeを自動実装します。(1, 2) == (1, 2)は true です。
📖 まとめ
- Records は軽量な匿名データ集約で、位置指定フィールドと名前付きフィールドをサポートし、値の等価性を持つ。
- パターンマッチングは if-case、switch パターン、変数分解、ガード条件をサポートする。
- シールクラスはサブクラスを同じライブラリに制限し、コンパイラが網羅的 switch を保証する。
- 三矢の活用:シールが有限型を定義 → パターンがマッチして分解 → Records が軽量戻り値を提供。
- これは Dart 3 の最も中核的なアップグレードであり、コードをより安全、簡潔、表現豊かにする。
📝 練習問題
- 基礎(難易度 ⭐):2 つの Record を定義してください:注文 ID と金額を表す位置指定 Record
(String, double)、および({String id, double amount, String status})名前付き Record。インスタンスを作成し、分解して出力してください。 - 中級(難易度 ⭐⭐):3 つの支払い方法(CreditCard/PayPal/BankTransfer)を定義するシールクラスを使用してください。それぞれ異なるフィールドを持ちます。switch 式を使って
describePayment()メソッドを実装し、網羅性を保証します。 - 挑戦(難易度 ⭐⭐⭐):完全な API レスポンス処理システムを設計してください:レスポンスタイプ(Success/ValidationError/ServerError/Timeout)を定義するシールクラスを使用し、それぞれが Record データを運ぶようにします。関係パターンとガード条件を取り入れたパターンマッチングを使ってレスポンスハンドラを実装してください。