本ツールで使用しているオープンソースライブラリ

本ツールのコードには 1 個のオープンソースライブラリが含まれています。

Dart チートシート — 簡潔なリファレンス

Dart 3 の文法、型システム、非同期、そして最もよく使う標準ライブラリをまとめたチートシートで、日常の 80%程度をカバーします。

D

Dart Dart 3

Dart SDK · オブジェクト指向 · ジェネリック · 関数型 · 非同期ファースト · 静的(健全な null 安全)

おすすめの学習パス

まず dart create/run と pubspec の構成を学ぶ → 変数・型・制御フローを習得 → 関数・文字列・コレクションを掘り下げる → クラス、ミキシン、エラー処理を理解する → isolate と async/await で並行処理を扱う → 必要に応じてネットワーク、時間、プロセス、正規表現、ビルド・テストを学ぶ、という順です。FAQ 章は後から落とし穴を避けるのに便利です。

1.Hello World と実行環境

最小プログラム、dart create/run とプロジェクト構成。

最小プログラム

すべての Dart プログラムは void main() のエントリ関数から始まります。dart run で実行され、print で標準出力に出力します。

1
2
3
4
5
6
7
8
9
void main() {
print('Hello, world!');
}
// 実行方法:
// dart run hello.dart
// または先にコンパイルして実行:
// dart compile exe hello.dart -o hello
// main は main という名前でないとエントリとして扱われない
// 戻り値型 void は値を返さないことを表す

作成と実行

dart create がプロジェクトの雛形を作り、dart run がエントリファイルを直接実行します。開発時は JIT でホットリロード、リリース時は AOT でコンパイルします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// コンソールプロジェクトを作成:
// dart create -t console myapp
// Flutter プロジェクトを作成:
// flutter create myapp
// プロジェクトディレクトリへ:
// cd myapp
// エントリファイルを実行:
// dart run
// ファイルを指定して実行:
// dart run bin/main.dart
// 実行ファイルにコンパイル:
// dart compile exe bin/main.dart
// 静的解析:
// dart analyze

プロジェクト構成

pubspec.yaml にパッケージ名、依存関係、SDK バージョンを宣言します。コードは lib/ と bin/ に、テストは test/ に置きます。

1
2
3
4
5
6
7
8
9
10
11
12
// pubspec.yaml の例:
// name: myapp
// environment:
// sdk: '>=3.0.0 <4.0.0'
// dependencies:
// http: ^1.1.0
// ディレクトリの慣例:
// lib/main.dart ライブラリコード
// bin/main.dart 実行可能エントリ
// test/ 単体テスト
// 依存をインストール:
// dart pub get

print 出力

print は stdout に出力し改行を追加します。$ で変数を補間し、${式} は式を埋め込みます。

1
2
3
4
5
6
7
8
9
10
void main() {
print('Hello, Dart!');
final name = 'Rex';
print('Hi, $name'); // 変数補間
final age = 5;
print('Age is ${age + 1}'); // 式の補間
print('Pi = ${3.14159.toStringAsFixed(2)}');
}
// stdout.write は改行しない
// stderr.writeln はエラーストリームへ出力

コマンドライン引数

main は List<String> args を受け取れます。args[0] 以降がコマンドラインの引数で、プログラム名は含まれません。

1
2
3
4
5
6
7
8
9
10
11
void main(List<String> args) {
print('参数个数: ${args.length}');
for (final arg in args) {
print('参数: $arg');
}
}
// 実行:
// dart run hello.dart a b c
// args == ['a', 'b', 'c']
// プログラム名自体は args に入らない
// プログラムのパスが必要なら Platform.script

非同期 main 関数

main は Future<void> でもよく、すべての await が完了するまで待ってから終了します。時間のかかる処理でよく使います。

1
2
3
4
5
6
7
8
9
Future<void> main() async {
print('开始');
await Future.delayed(Duration(seconds: 1));
print('一秒后执行');
}
// Future.delayed で時間のかかるタスクを模擬
// await は Future の完了までブロック
// Future<void> を返すとランタイムが待機
// 待たないとプログラムが先に終了し得る

コメント

// は単行コメント、/// はドキュメントコメント(dart doc 用)、/* */ はブロックコメントです。実行には影響しません。

1
2
3
4
5
6
7
8
9
// 単一行コメント
/// ドキュメントコメント:dart doc で API ドキュメントを生成
/* ブロックコメント:1 行で閉じればよい */
void main() {
// 行末コメント
print('OK'); // この行が何をするか説明
}
// ドキュメントコメントは宣言の前に置く
// dart doc で HTML ドキュメントを生成

トップレベルメンバー

Dart ではトップレベルの関数と変数が使えます(クラスの外側)。import でライブラリを読み込み、トップレベル変数はファイル間で共有されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:math';
// トップレベル変数:
final appName = 'demo';
// トップレベル関数:
int square(int x) => x * x;
void main() {
print(appName);
print(square(4));
print(max(1, 9));
}
// トップレベルメンバーはライブラリ全体で可視
// 変数は final にしてグローバルな可変化を避ける
// プライベートメンバーはアンダースコア始まり:_helper
// import はファイルの最上部に置く

2.変数と定数

変数宣言、final/const、null 安全と分割代入。

var と final

var は再代入可能ですが、推論される型は固定されます。final は一度だけ代入でき、以降は変更できません。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
var counter = 0; // int と推論される
counter = 1; // 再代入可
final name = 'Rex'; // 一度しか代入できない
// name = 'New'; // エラー:final は変更不可
final now = DateTime.now(); // 実行時に確定
print('$counter $name $now');
}
// var は一度型を推論したら固定
// 型が違うとエラー:counter = 'x'
// final は代入回数のみ制約
// オブジェクトの中身は変更可能

const 定数

const はコンパイル時定数で、コンパイル時に値が確定している必要があります。同じ const 値は正規化されて 1 つのインスタンスになります。

1
2
3
4
5
6
7
8
9
10
11
void main() {
const pi = 3.14159;
const greeting = 'Hello';
const list = [1, 2, 3]; // コンパイル時リスト
const double factor = 0.5;
print('$pi $greeting $list $factor');
}
// const の値はコンパイル時に既知である必要がある
// DateTime.now() は const に使えない
// const コンストラクタはコンパイル時定数を作る
// 優先的に const を使い性能を上げる

明示的な型

変数には明示的に型を宣言できます。静的型はコンパイル時のチェックに有効で、公開 API では明示を推奨します。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
int count = 42;
double ratio = 3.14;
String name = 'Dart';
bool enabled = true;
List<int> nums = <int>[1, 2, 3];
print('$count $ratio $name $enabled $nums');
}
// 型は変数名の前に書く
// 明示的な型で意図がはっきりする
// ローカル変数は var で推論させられる
// 引数と戻り値には型を書くのが推奨

null 許容変数

健全な null 安全では既定で null 不可です。型に ? を付けると null 許容になり、null チェックや ?? で既定値を使います。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String? maybe = null; // null 許容
String name = maybe ?? '匿名'; // 空合体でデフォルト値
if (maybe != null) {
print('有值: $maybe');
}
print(name);
}
// String? は null を代入できる
// String は null を代入できない(コンパイルエラー)
// ?? は左側が null のとき右側を使う
// 判空後 Dart は型昇格を行う

late 遅延初期化

late は初回アクセスまで初期化を遅らせます。late final は一度だけ代入でき、ランタイム値に依存するフィールドに適しています。

1
2
3
4
5
6
7
8
9
10
11
12
class Config {
late final String apiKey = loadKey();
}
String loadKey() => 'abc-123';
void main() {
final config = Config();
print(config.apiKey); // 初回アクセス時にロード
}
// late は宣言時に即座に代入しなくてよい
// 初回使用までに代入が必須、さもないとエラー
// late final は一度しか初期化されない
// シングルトン、遅延読み込み設定に使う

分割代入

Dart 3 ではレコードとパターンマッチによる分割代入ができ、コレクション要素を一度に複数の変数に代入できます。形状が一致する必要があります。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final (x, y) = (10, 20);
final (name: n, age: a) = (name: 'Rex', age: 5);
final [first, second, ...] = [1, 2, 3];
print('$x $y $n $a $first $second');
}
// 余った要素は _ で無視:
// final [_, second2] = [1, 2];
// rest がない場合、リストパターンの長さは一致必須
// map のキーと値にも適用可能
// for (final MapEntry(k: k, v: v) in entries)

スコープとシャドウイング

変数はそのブロック内で参照でき、内側で外側の同名変数をシャドウイングできます。トップレベル変数はライブラリ全体で参照できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final top = '全局';
void main() {
final inner = '函数内';
if (true) {
final block = '块内';
print(block);
print(inner); // 外側のスコープが見える
print(top); // グローバルが見える
}
// print(block); // エラー:スコープ外
}
// ブロックスコープは波括弧と一致
// 内側は外側の同名変数を遮蔽できる
// 同じスコープで重複宣言はエラー
// 深い遮蔽は可読性を損なうので避ける

dynamic と Object

dynamic は静的チェックをスキップして実行時に型が決まります。Object はすべてのクラスの親で、明示的なキャストが必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
dynamic d = 42;
d = '变成字符串'; // 実行時に任意の型
Object obj = 42;
// obj.length; // エラー:Object にそのメンバーはない
if (obj is String) {
print(obj.length); // 型昇格後アクセス可能
}
print(d);
}
// dynamic は静的検査をしない、慎重に使う
// Object はすべてのクラスの親型
// is 判定後、型昇格が起こる
// 明確な型かジェネリクスを優先

3.データ型

組み込み型、コレクション型、レコード、列挙型。

数値型

int が整数、double が浮動小数、num は両方の親型です。int リテラルは double に代入できます。

1
2
3
4
5
6
7
8
9
10
11
void main() {
int i = 42;
double d = 3.14;
num n = 42; // num は整数も保持可能
double whole = 42; // int は直接 double に代入可
print('$i $d $n $whole');
}
// int はネイティブプラットフォームで 64 ビット
// double は 64 ビット IEEE 754
// 数値演算の結果型はオペランドに依存
// 型判定は is:if (n is int)

文字列型

String は不変の UTF-16 シーケンスです。シングルクォートとダブルクォートは等価で、補間と複数行文字列もサポートします。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String a = '单引号';
String b = "双引号"; // 等価
String multi = '''第一行
第二行''';
print('$a $b');
print(multi);
}
// 文字列は変更不可
// 変更操作はすべて新しい文字列を返す
// 一方の引用符の中に他方の引用符を直接入れられる
// 同じ引用符が必要ならバックスラッシュでエスケープ

ブール型

bool は true と false のみです。条件式は bool を返さなければならず、数の暗黙的変換はありません。

1
2
3
4
5
6
7
8
9
10
11
void main() {
bool ok = true;
bool done = false;
if (ok) print('成功');
final result = ok && !done;
print(result);
}
// && 論理積 || 論理和
// ! 論理否定
// 三項:ok ? '是' : '否'
// Dart に truthy/falsy の概念はない

リスト型

List は順序付きのコレクションで、ジェネリックをサポートします。リテラル [a, b] で作成し、既定では拡張可能です。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
List<int> nums = [1, 2, 3];
nums.add(4);
nums.remove(2);
final first = nums.first;
final len = nums.length;
print('$first $len $nums');
}
// List<String> のジェネリクスで要素型を限定
// インデックスアクセス nums[0]、範囲外は例外
// isEmpty / isNotEmpty で空判定
// first / last で先頭と末尾を取得

セットとマップ

Set は順序なしで重複を許さない集合、Map はキーと値の対応です。どちらもジェネリックとリテラル構文を持ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
Set<String> tags = {'a', 'b', 'a'}; // 自動で重複除去
tags.add('c');
Map<String, int> ages = {'alice': 30};
ages['bob'] = 25;
print(tags); // {a, b, c}
print(ages['bob']); // 25
print(ages['nobody']); // null
}
// Set のメンバー判定は contains
// Map は存在しないキーに null を返す
// Map の走査順は挿入順
// Set は重複除去とメンバー判定に適する

レコード型

Dart 3 のレコードは名前のない軽量な集約型です。フィールド名を付けられ、パターン分割代入にも対応します。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
var point = (x: 1, y: 2); // 名前付きレコード
var pair = ('left', 3); // 位置レコード
print(point.x);
print(pair.$1); // 位置での取得
final (a, b) = ('first', 2);
print('$a $b');
}
// レコードは自動で == と hashCode を得る
// レコードの型は形状:(int, int)
// 小さなクラスの代わりに複数値を返すのに適する
// レコードは不変

列挙型

enum は限られた名前付き定数の集合を定義します。拡張列挙(Dart 2.17+)はフィールドとメソッドを持てます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Status { pending, approved, rejected }
enum Color {
red(255), green(0), blue(0);
const Color(this.value);
final int value;
}
void main() {
final s = Status.approved;
print(s.name); // approved
print(Color.red.value);
for (final c in Status.values) {
print(c.name);
}
}
// .name で列挙名を取得
// .values ですべてのメンバーを走査
// switch で列挙を扱うと既定で網羅的
// 強化列挙はデータと振る舞いを付加できる

型変換

int.parse / double.parse で文字列を数値に変換し、toString で文字列へ戻す。as は実行時の型変換を行う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
int n = int.parse('42');
double d = double.parse('3.14');
String s = 42.toString();
Object obj = 'hello';
if (obj is String) {
final len = obj.length; // 昇格後は安全
print(len);
}
print('$n $d $s');
}
// parse が失敗すると FormatException を投げる
// toStringAsFixed(2) は小数第 2 位まで残す
// as の強制キャスト失敗は TypeError
// as より is + 型昇格を推奨

4.参照と null 安全

オブジェクト参照、null 安全、ディープコピーとネイティブメモリ。

オブジェクト参照

Dart には生ポインタがなく、変数はオブジェクト参照を保持します。代入は参照をコピーするだけで、複数の変数が同じオブジェクトを指せます。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final list1 = [1, 2, 3];
final list2 = list1; // 参照をコピー
list2.add(4);
print(list1); // [1, 2, 3, 4]
final copy = List.of(list1); // 実際にコピー
copy.add(5);
print(list1); // 影響なし
}
// 参照型の代入はオブジェクトを共有
// List.of / [...list] は浅いコピー
// 基本型は不変の値型
// 可変オブジェクトを共有するときは副作用に注意

null 安全

健全な null 安全により、null は String? のような null 許容型にのみ現れます。非 null 性はコンパイル時に保証されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String name = 'Rex'; // 非 null
String? nickname; // null 許容、既定は null
nickname = '小雷';
if (nickname != null) {
print('$name 的昵称 $nickname');
}
print(nickname);
}
// 非 null 型に null を代入するとコンパイルエラー
// 判空後、変数は自動で非 null に昇格
// ?? はデフォルト値を与えて判空を避ける
// ?. 安全アクセス:nickname?.length

null 表明 !

null 許容式の後に ! を付けると非 null を表明します。失敗時は null チェック例外が投げられます。確実な場合だけ使ってください。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
String? maybe = '有值';
final len = maybe!.length; // 非 null を断言
print(len);
String? empty;
final safe = empty?.length; // null
print(safe);
}
// ! は null 安全チェックを迂回
// 断言失敗は Null check error
// 判空と型昇格を優先
// ?. 安全アクセスは例外でなく null を返す

null 合体

?? は左辺が null のとき右辺を返します。??= は変数が null のときだけ代入します。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String? user;
final name = user ?? '匿名'; // 空合体
user ??= '游客'; // null のときだけ代入
int? count;
count ??= 0;
count++;
print('$name $user $count');
}
// ?? は左側が null かどうかのみ見る
// ??= は count = count ?? 0 と等価
// 連結可能:a ?? b ?? c ?? '默认'
// ?? はコレクションのデフォルトにも使う:list ?? []

late と const

final はランタイムで 1 回だけ代入、const はコンパイル時定数です。late は大きなオブジェクトや循環依存の遅延初期化に適しています。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final now = DateTime.now(); // 実行時
const pi = 3.14159; // コンパイル時
late final big = createBig(); // 初回アクセス時
print('$now $pi ${big.length}');
}
List<int> createBig() => List<int>.generate(100, (i) => i);
// final はコンストラクタや実行時にも代入可
// const の値はコンパイル時に確定必須
// late は初回アクセスまで遅延
// トップレベルと静的変数は既定で遅延初期化

参照と等価性

== は既定では参照比較で、identical() で同一インスタンスかを判定します。構造的等価性にはヘルパー関数を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final a = [1, 2];
final b = [1, 2];
print(a == b); // false:別のオブジェクト
print(identical(a, b)); // false:別の参照
final c = a;
print(identical(a, c)); // true:同じ参照
print(a.length == b.length); // true
}
// 文字列と数値は値セマンティクス、== は内容を比較
// List/Map は既定で参照比較
// 構造比較は listEquals(package:collection)
// 独自クラスは == と hashCode をオーバーライド可

ディープコピーとシャローコピー

シャローコピーは最上位のみを複製し、深い階層のオブジェクトは共有されます。ディープコピーは階層ごとに複製し、不変コレクションは共有しても安全です。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nested = [[1], [2]];
final shallow = List.of(nested); // 浅いコピー
shallow[0].add(9);
print(nested[0]); // [1, 9]
final deep = nested.map((e) => List.of(e)).toList();
deep[0].add(8);
print(nested[0]); // [1, 9] 変わらない
}
// 浅いコピーはネストしたオブジェクトを共有
// 深いコピーは各層で新規オブジェクトを作る
// Map.of / Set.of も同様
// 不変コレクションは共有副作用を防ぐ

ネイティブメモリ

dart:ffi は Pointer と calloc を提供し、C ライブラリとの相互運用のためにネイティブメモリへアクセスします。ポインタの寿命は開発者が管理します。

1
2
3
4
5
6
7
8
9
10
11
12
// dart:ffi はネイティブメモリへのアクセスを提供:
// Pointer<Int32> は C の int ポインタを表す
// 割り当てと解放:
// final p = calloc<Int32>(1); // package:ffi
// p.value = 42;
// calloc.free(p);
// C ライブラリとの相互運用:
// final lib = DynamicLibrary.open('libc.so')
// final fn = lib.lookupFunction(...)
// 文字列は Pointer<Utf8>
// ネイティブプラットフォームのみ、Web では不可
// ポインタのライフサイクルは開発者が管理

5.制御フロー

if、ループ、switch、パターンマッチング。

if / else

if/else if/else は条件によって分岐します。条件は bool である必要があり、null チェックで型昇格が発生します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
int score = 85;
if (score >= 90) {
print('优秀');
} else if (score >= 60) {
print('及格');
} else {
print('不及格');
}
}
// 条件は bool を返さなければならない
// 波括弧は省略できるが付けるのが推奨
// 判空後の自動型昇格:
// if (name != null) print(name.length)
// else がない場合、条件不成立ならスキップ

三項演算子

条件 ? 真の値 : 偽の値 は単一式による分岐です。?? と組み合わせて null 既定値を扱えます。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final n = 7;
final sign = n > 0 ? '正' : '非正';
String? maybe;
final label = maybe ?? '默认'; // 空合体
print(sign);
print(label);
}
// 三項の二つの分岐の型は一致させる
// ネストした三項は可読性が低いので避ける
// ?? は null デフォルト値を専門に扱う
// 複雑な分岐は if/else の方が明確

for ループ

for の古典的な 3 段形式と、for-in による Iterable の反復が使えます。Dart 3 ではループ変数のパターン分割代入もサポートされます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
for (var i = 0; i < 3; i++) {
print('i=$i');
}
final names = ['Rex', 'Ada'];
for (final name in names) {
print(name);
}
for (final (i, name) in names.indexed) {
print('$i: $name');
}
}
// i++ は i = i + 1 と等価
// for-in は Iterable と Map を走査可能
// .indexed は (index, value) レコードを生成
// 走査中のコレクション変更は慎重に

while ループ

while は判定後に実行、do-while は最低 1 回実行してから判定します。反復回数が未知のループに向きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
var n = 3;
while (n > 0) {
print('n=$n');
n--;
}
var count = 0;
do {
print('至少执行一次');
count++;
} while (count < 1);
}
// while は条件が false なら一度も実行しない
// do-while は最低 1 回実行
// 条件は bool でなければならない
// ループ変数を更新して無限ループを防ぐ

switch 式

Dart 3 の switch 式は値を返します。case の後にはパターンを書き、=> で結果を返します。break は不要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final status = 404;
final label = switch (status) {
200 => '成功',
404 => '未找到',
_ => '其他',
};
print(label);
}
// 末尾は網羅するか _ でフォールバック
// 式版の case は : でなく => を使う
// パターン対応:switch (value) { int n => ... }
// if 連鎖より簡潔、結果を代入できる

switch 文

switch 文は case で分岐します。空 case のフォールスルー、ガード節、パターンをサポートします。非空の分岐では break が必要です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void main() {
final day = 3;
switch (day) {
case 1:
case 2:
case 3:
print('工作日');
break;
default:
print('周末');
}
switch (day) {
case int n when n > 3:
print('大于 3');
break;
default:
print('其他');
}
}
// 空の case は自動で下へフォールスルー
// 空でない分岐は break か return が必要
// when で追加の条件ガードを付ける
// パターンマッチでは if 連鎖より強力

パターンマッチング

if-case と switch は値に構造マッチングを行います。オブジェクトパターン、レコードパターン、ガードを組み合わせて使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Circle {
const Circle(this.radius);
final double radius;
}
void main() {
final shape = Circle(5);
if (shape case Circle(radius: final r)) {
print('半径 $r');
}
final value = (name: 'Rex', age: 5);
final msg = switch (value) {
(name: final n, age: 0) => '$n 刚出生',
(name: final n, age: final a) => '$n $a 岁',
_ => '未知',
};
print(msg);
}
// if-case はマッチ成功時のみ分岐に入る
// オブジェクトパターンはフィールド名で分解
// レコードパターンは位置か名前でマッチ
// 網羅的 switch はコンパイラがカバーを保証

break と continue

break は現在のループを抜け、continue は次の反復へ進みます。ラベルでネストしたループのジャンプ先を制御できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main() {
for (var i = 0; i < 5; i++) {
if (i == 1) continue; // i=1 をスキップ
if (i == 4) break; // ループを抜ける
print('i=$i'); // 0 2 3
}
outer:
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
if (j == 1) break outer; // 外側を抜ける
print('$i,$j');
}
}
}
// continue は今の周の残りだけをスキップ
// break はループを完全に終了
// ラベルはループの前に書き、ラベル名で参照
// ラベルは深いネストからの脱出に適する

6.関数とラムダ

関数定義、オプションパラメータ、クロージャ、非同期関数。

関数の定義

関数は戻り値の型、名前、パラメータ、本体で構成されます。戻り値がない関数は void を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int add(int a, int b) {
return a + b;
}
void greet(String name) {
print('你好,$name');
}
void main() {
print(add(1, 2));
greet('Rex');
}
// 戻り値型は関数名の前に書く
// 引数には型が必須
// 戻り値がなければ void
// 関数はオブジェクト、変数に代入できる

アロー関数

単一式の本体の関数は => で簡潔に書け、その式の値が返ります。純粋関数やコールバックに適しています。

1
2
3
4
5
6
7
8
9
10
11
12
int square(int x) => x * x;
bool isEven(int n) => n % 2 == 0;
void main() {
print(square(4));
print(isEven(3));
final doubled = [1, 2, 3].map((n) => n * 2).toList();
print(doubled);
}
// => の関数本体は単一の式でなければならない
// { return expr; } と等価
// コールバックや高階関数によく使う
// void を返す:() => print('x')

オプショナル位置引数

角括弧 [] はオプショナル位置引数で、省略時は既定値または null。既定値はコンパイル時定数でなければなりません。

1
2
3
4
5
6
7
8
9
10
11
12
String join(String a, [String b = '无', int times = 1]) {
return '$a $b 重复 $times 次';
}
void main() {
print(join('你好'));
print(join('你好', '世界'));
print(join('你好', '世界', 3));
}
// [] 内の引数は位置による任意引数
// デフォルト値は引数の後に = で書く
// デフォルト値がない場合は型は null 許容
// null 許容の任意引数は ?? でデフォルトを指定

名前付き引数

波括弧 {} は名前付き引数で、呼び出し時は名前で指定します。required を付けると必須になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
String describe(String name,
{int age = 0, String? city, required bool married}) {
return '$name 年龄 $age 城市 $city 已婚 $married';
}
void main() {
final s = describe('Rex',
age: 5, city: '上海', married: false);
print(s);
}
// 名前付き引数は順序に依存しない
// 渡さなければデフォルト値か null
// required は明示的に渡すことが必須
// 長い呼び出しは改行して可読性を高める

高階関数

関数は第一級市民で、引数として渡したり戻り値として返せます。map/where/fold はコレクションでよく使う高階関数です。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final nums = [1, 2, 3, 4];
final doubled = nums.map((n) => n * 2).toList();
final evens = nums.where((n) => n.isEven).toList();
final sum = nums.fold(0, (acc, n) => acc + n);
print('$doubled $evens $sum');
}
// map は要素ごとに変換
// where は条件でフィルタ
// fold は単一の結果へ累積
// 結果は遅延 Iterable、toList で固定

クロージャ

クロージャは定義環境の変数を捕捉します。外側の関数が返った後も、これらの変数へアクセス・変更できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function makeCounter() {
var count = 0;
return () => ++count;
}
void main() {
final counter = makeCounter();
print(counter()); // 1
print(counter()); // 2
final counter2 = makeCounter();
print(counter2()); // 1:独立した状態
}
// クロージャは環境と状態を保持
// makeCounter を呼ぶたび独立したクロージャを生成
// 変数が閉包間で共有されるか独立かは位置による
// 状態のカプセル化に使う

非同期関数

async 関数は Future を返し、内部で await により結果を待ちます。await は async 関数内でのみ使用できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
Future<String> fetchData() async {
await Future.delayed(Duration(milliseconds: 100));
return '数据';
}
Future<void> main() async {
print('开始');
final data = await fetchData();
print(data);
}
// async 関数は暗黙に Future<T> を返す
// await は Future 完了まで待機
// 例外は await の場所に伝わり try/catch できる
// await を忘れると Future オブジェクト自体を受け取る

ジェネレータ

sync* は遅延 Iterable を生成し、async* は Stream を生成します。yield で値を 1 つずつ出し、yield* で他のジェネレータに委譲します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Iterable<int> countUp(int max) sync* {
for (var i = 0; i < max; i++) {
yield i;
}
}
Stream<int> streamCount(int max) async* {
for (var i = 0; i < max; i++) {
await Future.delayed(Duration(milliseconds: 10));
yield i;
}
}
void main() {
print(countUp(3).toList());
}
// sync* は逐次生成でメモリを占有しない
// async* は await と組み合わせストリームを生成
// yield* は部分シーケンスを展開:yield* countUp(2)
// ジェネレータは必要なときに実行、初回アクセスで走る

7.文字列

リテラル、補間、部分文字列、エンコーディングとフォーマット。

文字列リテラル

シングルクォートとダブルクォートはいずれも文字列です。バックスラッシュでエスケープでき、r プレフィックスでraw文字列になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
String s1 = '单引号';
String s2 = "双引号";
String esc = '换行符\n制表符\t';
String raw = r'不转义\n直接显示';
print('$s1 $s2');
print(esc);
print(raw);
}
// \n 改行 \t タブ \\ バックスラッシュ
// r'...' 生文字列はエスケープを処理しない
// 正規表現、Windows パスに適する
// 引用符の中でもう一方の引用符を使いエスケープを回避

文字列補間

$変数 や ${式} で値を文字列に埋め込みます。任意のオブジェクトは toString が呼ばれます。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final name = 'Rex';
final age = 5;
print('$name 今年 $age 岁');
print('十年后 ${age + 10} 岁');
final list = [1, 2];
print('共 ${list.length} 个元素');
}
// $name で変数を直接挿入
// ${...} で式と呼び出しを挿入
// オブジェクトは自動で toString 変換
// 文字列連結より明確で効率的

複数行文字列

3 つのシングルクォート ''' で複数行文字列を作ります。改行とインデントがそのまま保たれます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
String poem = '''
床前明月光,
疑是地上霜。''';
print(poem);
String html = '''
<div>
<p>你好</p>
</div>''';
print(html);
}
// 複数行文字列は実際の改行を保持
// 先頭と末尾の改行も含まれる
// 補間と組み合わせて使える
// SQL、HTML、テンプレートテキストに適する

よく使うメソッド

contains/startsWith で包含判定、replaceAll で置換、toUpperCase/toLowerCase で大小文字を変換します。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final text = 'Hello Dart';
print(text.contains('Dart')); // true
print(text.startsWith('Hello')); // true
print(text.toLowerCase()); // hello dart
print(text.replaceAll('Dart', 'Flutter'));
print(text.replaceFirst('l', 'L')); // HeLlo Dart
print(text.length);
}
// 文字列メソッドは新しい文字列を返し、元を変更しない
// trim() は前後の空白を除去
// isEmpty / isNotEmpty で空判定
// メソッドはチェーン呼び出し可能

部分文字列と検索

substring で位置を指定して切り出し、indexOf で部分文字列を検索、split で区切り文字ごとに分割します。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final text = 'hello world';
print(text.substring(0, 5)); // hello
print(text.substring(6)); // world
print(text.indexOf('world')); // 6
print(text.indexOf('x')); // -1 未找到
print(text.split(' ')); // [hello, world]
}
// substring の引数は開始(含む)と終了(含まない)
// indexOf は見つからなければ -1 を返す
// lastIndexOf は後ろから探す
// split の結果は区切り文字自体を含まない

文字とエンコーディング

Dart の文字列は UTF-16 で、codeUnits はコードユニット、runes はコードポイントを返します。emoji は runes が必要です。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final s = 'A中😀';
print(s.length); // 4 个 UTF-16 单元
print(s.codeUnits); // 编码单元列表
print(s.runes.toList()); // 码点列表
print(String.fromCharCodes(s.runes));
}
// 中国語と emoji は複数の UTF-16 ユニットを占める
// length は文字数でなく符号単位の数を返す
// runes は Unicode コードポイントを返す
// 実際の文字数が必要なら runes.length を使う

効率的な連結

ループ内で文字列を連結すると一時オブジェクトが大量に作られます。StringBuffer で蓄積してから一括で文字列化します。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final buffer = StringBuffer();
for (var i = 0; i < 1000; i++) {
buffer.write('第 $i 行\n');
}
final result = buffer.toString();
print('${result.length} 个字符');
}
// write は改行なしで追記
// writeln は改行付きで追記
// toString で一度に文字列を生成
// + での連結よりはるかに効率的

フォーマットとパース

toStringAsFixed で小数桁を固定し、padLeft で桁を揃え、int.parse/double.parse で数字文字列をパースします。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final pi = 3.14159;
print(pi.toStringAsFixed(2)); // 3.14
print(7.toString().padLeft(3, '0')); // 007
final n = int.parse('42');
final d = double.parse('3.14');
print('$n $d');
}
// toStringAsFixed は四捨五入
// padLeft/padRight で文字を埋める
// parse の失敗は FormatException
// 基数を指定:int.parse('ff', radix: 16)

8.コレクション

リスト、マップ、集合作業とソート。

リストの操作

add で追加、insert で挿入、remove で削除、sort で並べ替えます。既定の List は可変で拡張可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nums = [3, 1, 2];
nums.add(4); // [3, 1, 2, 4]
nums.insert(0, 0); // [0, 3, 1, 2, 4]
nums.remove(3); // 移除元素 3
nums.sort(); // 升序
print(nums);
print(nums.reversed.toList());
}
// remove は最初の等しい要素を削除
// removeAt はインデックスで削除
// contains は含むか判定
// indexOf は最初に一致する位置を検索

リストの高階関数

map で変換、where で抽出、reduce/fold で集約、expand で平滑化します。返される Iterable は多くが lazy です。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final nums = [1, 2, 3, 4];
final doubled = nums.map((n) => n * 2).toList();
final evens = nums.where((n) => n.isEven).toList();
final sum = nums.reduce((a, b) => a + b);
final flat = [[1, 2], [3]].expand((e) => e).toList();
print('$doubled $evens $sum $flat');
}
// map/where は遅延 Iterable を返す
// 遅延シーケンスは走査時のみ計算
// reduce はリストが空だとエラー
// fold は初期値を与えられて安全

スプレッド演算子

... はコレクションの要素をリテラルに展開します。...? はソースが null のとき安全にスキップします。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final a = [1, 2];
final b = [3, 4];
final merged = [...a, ...b, 5]; // [1,2,3,4,5]
List<int>? maybe;
final safe = [...a, ...?maybe]; // 空安全
final copy = [...a]; // 浅拷贝
print('$merged $safe $copy');
}
// スプレッド演算子はサブリストでなく要素を展開
// ...? は null のソースを自動処理
// Map と Set リテラルでも使える
// コレクションの結合・コピーによく使う

コレクション内 if/for

コレクションリテラル内で if により要素を含めたり、for でまとめて生成できます。Dart 3 ではパターンマッチも使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
final includeZero = true;
final nums = [
1,
if (includeZero) 0,
for (var i = 2; i < 4; i++) i,
];
print(nums); // [1, 0, 2, 3]
final pairs = {
for (var i = 0; i < 3; i++) 'k$i': i,
};
print(pairs);
}
// if が不成立ならその要素は省略
// for ループは複数の要素を生成
// Set と Map にも同様に使える
// データの記述を宣言的にできる

マップの操作

Map のキー、値、エントリは反復可能です。putIfAbsent は必要になってから埋め、update は更新、remove は削除します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final ages = {'alice': 30, 'bob': 25};
ages['carol'] = 22; // 新增
ages.update('bob', (v) => v + 1); // 更新
ages.putIfAbsent('dave', () => 40);
ages.remove('alice');
for (final entry in ages.entries) {
print('${entry.key}: ${entry.value}');
}
}
// 存在しないキーへのアクセスは null を返す
// putIfAbsent は欠けているときだけ計算
// entries はキーと値のペアを提供
// containsKey はキーの有無を判定

セットの操作

Set は要素の一意性を保証します。union/intersection/difference で集合演算ができ、toSet でリストの重複を除去します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final a = {1, 2, 3};
final b = {3, 4, 5};
print(a.union(b)); // {1,2,3,4,5}
print(a.intersection(b)); // {3}
print(a.difference(b)); // {1,2}
final nums = [1, 1, 2, 3];
final unique = nums.toSet();
print(unique);
}
// Set は順序なし、要素は一意
// contains でのメンバー判定が高速
// Set で重複除去とメンバー検査ができる
// add は既存要素なら何もしない

ソートと検索

sort は既定で昇順で、比較器を渡せます。indexOf は線形探索、contains は存在判定です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
final names = ['bob', 'alice', 'carol'];
names.sort(); // 字母序
print(names);
final people = [
('bob', 30),
('alice', 20),
];
people.sort((x, y) => x.$2.compareTo(y.$2));
print(people);
print(names.indexOf('bob'));
}
// 既定では Comparable の順序
// 比較関数は負/ゼロ/正を返す
// sort はリストを破壊的に変更
// 元の順序が要るならコピーしてからソート

読み取り専用コレクション

List.unmodifiable は読み取り専用リストを作成し、変更すると UnsupportedError を投げます。List.of は可変リストをコピーします。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final frozen = List.unmodifiable([1, 2, 3]);
// frozen.add(4); // 抛 UnsupportedError
final editable = List.of([1, 2, 3]); // 可变副本
editable.add(4);
print(editable);
final constList = const [1, 2, 3]; // 编译期常量
print(frozen.length);
}
// unmodifiable は実行時の読み取り専用
// const はコンパイル時定数かつ正規化される
// API に渡すときは読み取り専用の方が安全
// Map.unmodifiable / Set.unmodifiable も同様

9.メモリとパフォーマンス

ガベージコレクション、const 正規化、バッファの再利用。

ガベージコレクション

Dart VM は自動でガベージコレクションを行い、手動の解放は不要です。参照されなくなったオブジェクトは回収対象になります。

1
2
3
4
5
6
7
8
9
10
void main() {
// 参照を失ったオブジェクトは回収の候補になる:
var data = List<int>.generate(10000, (i) => i);
data = [1, 2, 3]; // 古いリストはもう参照されない
print(data);
}
// 参照のないオブジェクトは GC が自動で回収
// 手動で delete する必要もできない
// 短命のオブジェクトが多いのは心配不要
// Flutter は世代別 GC を使用

const 正規化

const 値は正規化され、同じ定数は 1 つのインスタンスだけが存在します。const を活用して重複アロケーションを減らします。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
const a = [1, 2, 3];
const b = [1, 2, 3];
print(identical(a, b)); // true:同一实例
final c = [1, 2, 3];
final d = [1, 2, 3];
print(identical(c, d)); // false
}
// const は同じ値なら正規化して共有
// 非 const は毎回新しいオブジェクト
// ホットパスではできるだけ const リテラル
// コンパイル時定数は繰り返し構築されない

遅延初期化

トップレベル変数と static 変数は初回アクセスまで遅延初期化されます。late も同じ遅延セマンティクスを提供します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
print('开始');
print(cache); // 首次访问触发计算
print(cache); // 复用已有值
}
final cache = buildCache();
String buildCache() {
print('构建缓存中');
return 'heavy-data';
}
// トップレベル変数は初回アクセス時に初期化
// 以降は同じインスタンスを再利用
// late フィールドも同様
// 起動時の不要なコストを避ける

リストの容量

可変リストは必要に応じて拡張され、頻繁な add は何度も再アロケーションします。サイズが分かっていれば事前割り当てを推奨します。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final nums = <int>[];
for (var i = 0; i < 100; i++) {
nums.add(i); // 自动扩容
}
final sized = List<int>.filled(100, 0);
final growable = List<int>.generate(100, (i) => i);
print('${nums.length} ${sized.length} ${growable.length}');
}
// 既定のリストは成長可能、拡張にはコスト
// filled は固定長で初期化
// generate は関数で要素を生成
// 頻繁に追記するなら一括構築が効率的

バッファの再利用

Uint8List などの typed-data はバイナリバッファに適します。バッファを再利用し、StringBuffer で文字列を連結します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:typed_data';
void main() {
final bytes = Uint8List(4); // 固定大小
bytes[0] = 255;
final buffer = StringBuffer();
for (var i = 0; i < 10; i++) {
buffer.write(i);
}
print('$bytes ${buffer.toString()}');
}
// Uint8List はバイト単位アクセスが高速
// 大きなバイナリは List<int> でなく Uint8List
// バッファを再利用して GC 負荷を減らす
// 大きなデータは丸ごと読み込まずストリーム処理

弱参照関連付け

Expando はオブジェクト自体を変更せずにデータを付与し、キーが弱参照です。WeakReference は回収を妨げません。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final tags = Expando<String>();
final obj = Object();
tags[obj] = '元数据'; // 关联数据
print(tags[obj]); // 元数据
final weak = WeakReference(obj);
print(weak.target == obj); // true
}
// Expando のキーは弱参照
// オブジェクト回収後は関連データも消える
// キャッシュ、デバッグ、フレームワーク拡張に適する
// オブジェクト自身のフィールドを占有しない

オブジェクトのライフサイクル

オブジェクトは isolate ごとの独立ヒープに作られます。可変オブジェクトを isolate 間で共有できず、コピーかメッセージで渡します。

1
2
3
4
5
6
7
8
9
10
11
void main() {
// 每个 isolate 有独立堆
final local = [1, 2, 3];
// 发送给其它 isolate 的是副本:
// final result = await Isolate.run(() => local);
print(local);
}
// isolate 間でメモリは共有されない
// 状態の共有はメッセージパッシングかポート
// グローバルな可変オブジェクトは危険、慎重に
// データ処理の独立タスクに適する

ネイティブメモリ

C と相互運用する際は dart:ffi でネイティブメモリを割り当てます。手動で解放しないとメモリリークになります。

1
2
3
4
5
6
7
8
9
10
import 'dart:ffi';
// ポインタ型を宣言:
// typedef NativeBuf = Pointer<Uint8>;
// ネイティブメモリを割り当て:
// final p = calloc<Uint8>(1024); // package:ffi
// p[i] でバイトを読み書き
// 解放:
// calloc.free(p);
// 解放し忘れるとリークする
// ffi はネイティブプラットフォームのみ

10.オブジェクト指向

クラス、継承、mixin、インターフェース。

クラスとコンストラクタ

class はオブジェクトの青写真を定義します。コンストラクタはクラスと同名で、this. パラメータでフィールドに直接代入できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
String name;
int age;
Animal(this.name, this.age); // this パラメータ代入
void speak() {
print('$name 叫了一声');
}
}
void main() {
final dog = Animal('旺财', 3);
dog.speak();
print('${dog.name} ${dog.age} 岁');
}
// this.name でコンストラクタ引数をフィールドへ
// フィールドは既定で非 null、初期化必須
// メソッドはインスタンスフィールドにアクセス可
// インスタンス化に new キーワードは不要

継承

extends でスーパークラスを継承し、@override でメソッドを再定義し、super で親を呼び出します。Dart は単一継承です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
Animal(this.name);
final String name;
String sound() => '...';
}
class Dog extends Animal {
Dog(String name) : super(name);
@override
String sound() => '汪汪';
}
void main() {
final dog = Dog('旺财');
print(dog.sound());
}
// サブクラスは親のフィールドとメソッドを継承
// @override はオーバーライドを明示
// super(name) で親のコンストラクタに引数を渡す
// 1 つのクラスの親クラスは 1 つだけ

mixin

mixin は再利用可能な振る舞いの断片で、with で組み込みます。継承より柔軟で、単一継承の制限を避けられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mixin Swimmer {
void swim() => print('游泳');
}
mixin Flyer {
void fly() => print('飞行');
}
class Duck with Swimmer, Flyer {}
void main() {
final duck = Duck();
duck.swim();
duck.fly();
}
// with キーワードで複数の mixin を混入
// mixin は単体ではインスタンス化できない
// 親クラスを限定できる:mixin X on BaseClass {}
// 深い継承より振る舞いの合成が優れる

抽象クラス

abstract class はインスタンス化できず、契約だけを定義します。サブクラスは抽象メソッドを実装する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
abstract class Shape {
double area();
}
class Circle extends Shape {
Circle(this.r);
final double r;
@override
double area() => 3.14159 * r * r;
}
void main() {
final c = Circle(2);
print(c.area().toStringAsFixed(2));
}
// 抽象メソッドは本体を持たない
// 抽象クラスは具体的な実装を含める
// implements でインターフェース実装も可
// 型を統一すれば親クラスとして使える

インターフェース

すべてのクラスは暗黙的にインターフェースでもあり、implements で実装します。すべてのメンバーを再実装する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Greeter {
String greet(String name) => '你好,$name';
}
class EnglishGreeter implements Greeter {
@override
String greet(String name) => 'Hello, $name';
}
void main() {
final g = EnglishGreeter();
print(g.greet('Rex'));
}
// implements は実装を継承しない
// インターフェースの全メンバーをオーバーライド必須
// 1 つのクラスは複数のインターフェースを実装可能
// implements で型の契約を表現

アクセサ

get は読み取り専用アクセサ、set は書き込み可アクセサを定義します。呼び出しに括弧は不要で、内部実装を隠せます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Rectangle {
Rectangle(this.width, this.height);
double width;
double height;
double get area => width * height;
set scale(double factor) {
width *= factor;
height *= factor;
}
}
void main() {
final r = Rectangle(2, 3);
print(r.area); // 6.0
r.scale = 2;
print(r.area); // 24.0
}
// get はフィールドのように読み出す
// set はフィールドのように代入する
// get/set 内に検証ロジックを書ける
// 計算プロパティは get で導出

静的メンバー

static メンバーはインスタンスではなくクラスに属します。static メソッドはインスタンスメンバーにアクセスできません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MathUtils {
static const pi = 3.14159;
static int square(int x) => x * x;
static int _counter = 0; // 私有静态
static int get count => _counter;
}
void main() {
print(MathUtils.pi);
print(MathUtils.square(5));
}
// 静的メンバーはクラス名で直接アクセス
// static はインスタンスフィールドにアクセス不可
// プライベートメンバーはアンダースコア始まり、ライブラリ内で可視
// 共有設定には static 定数をよく使う

sealed クラス

sealed クラスはサブクラスを同一ライブラリに限定し、switch を default なしで網羅的にできます。Dart 3 の機能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}
String describe(Shape shape) => switch (shape) {
Circle() => '圆',
Square() => '方',
};
void main() {
print(describe(Circle()));
print(describe(Square()));
}
// sealed のサブクラスは同じライブラリ内に限定
// 網羅的 switch なら default 不要
// サブクラスを追加するとコンパイル警告
// パターンマッチと組み合わせて安全

11.エラー処理

try/catch、カスタム例外、非同期エラー。

try / catch

try 内で例外の可能性があるコードを包み、catch で捕捉します。catch (e) で例外オブジェクトを取得できます。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
try {
final n = int.parse('abc');
print(n);
} catch (e) {
print('解析失败: $e');
}
}
// catch (e) はすべての例外を捕捉
// 例外が投げられると try 内の残りはスキップ
// 捕捉されない例外はクラッシュを招く
// catch (e, st) でスタック st を取得可能

on 句

on 句で特定の例外型だけを捕捉し、catch と組み合わせて使います。型ごとに処理を分けられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
try {
throw FormatException('格式错误');
} on FormatException catch (e) {
print('格式: ${e.message}');
} on ArgumentError {
print('参数错误');
} catch (e) {
print('其他: $e');
}
}
// on 型 で一致する例外を捕捉
// catch のない on 分岐はオブジェクトを取らない
// 順にマッチし、先の方が優先
// フォールバックの catch が残りを捕捉

finally と rethrow

finally は例外の有無に関わらず実行され、リソース解放に適しています。rethrow は元のスタックを保ったまま再送出します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main() {
try {
outer();
} catch (e) {
print('最终处理: $e');
}
}
void outer() {
try {
throw Exception('原始错误');
} catch (e) {
print('中间捕获');
rethrow; // 保留堆栈继续抛
} finally {
print('清理资源');
}
}
// finally は必ず実行される
// rethrow は catch 内に置く必要がある
// rethrow は元のスタックを保持
// リソースや接続の解放によく使う

例外を投げる

throw キーワードで例外オブジェクトを送出します。何でも投げられますが、慣習的には Exception または Error のサブクラスを投げます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
try {
checkAge(-1);
} catch (e) {
print('拒绝: $e');
}
}
void checkAge(int age) {
if (age < 0) {
throw ArgumentError('年龄不能为负');
}
}
// throw は現在の制御フローを中断
// 例外は任意のオブジェクトでよい
// 慣例では Exception か Error
// 業務条件を満たさないなら例外を投げるのが推奨

カスタム例外

Exception を implements して業務例外を定義します。toString を上書きしてメッセージを分かりやすくし、フィールドを持たせられます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ValidationError implements Exception {
ValidationError(this.field, this.message);
final String field;
final String message;
@override
String toString() => '字段 $field 无效: $message';
}
void main() {
try {
throw ValidationError('email', '格式错误');
} on ValidationError catch (e) {
print(e.field); // email
print(e);
}
}
// implements Exception で例外と明示
// 構造化フィールドを保持できる
// on 節でカスタム型を正確に捕捉
// 上位で型ごとに分けて処理しやすい

非同期エラー

async 関数内の await が投げる例外は try/catch で捕捉できます。未処理の Future エラーは静かに失われます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Future<void> load() async {
throw Exception('加载失败');
}
Future<void> main() async {
try {
await load();
} catch (e) {
print('异步捕获: $e');
}
}
// await しないエラーは捕捉が難しい
// catchError でチェーン処理:
// load().catchError(print);
// 未処理エラーは zone に入り得る
// 統一的エラー処理方針を推奨

ストリームのエラー

Stream を listen する際は onError コールバックでエラーを扱います。StreamController.addError でストリームにエラーを注入できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:async';
void main() {
final controller = StreamController<int>();
controller.stream.listen(
(data) => print('数据: $data'),
onError: (e) => print('流错误: $e'),
onDone: () => print('流结束'),
);
controller.add(1);
controller.addError('意外错误');
controller.close();
}
// addError はエラーをストリームへ注入
// onError コールバックがストリームエラーを処理
// onDone はクローズ後に発火
// ストリームエラーは他のデータを中断しない

アサーション

assert は開発時に条件を検証し、失敗時に AssertionError を投げます。リリースビルドでは除去されます。

1
2
3
4
5
6
7
8
9
void main() {
final score = 85;
assert(score >= 0, '分数不能为负'); // 通过
print('断言通过');
}
// 条件が false なら AssertionError
// 第 2 引数は失敗メッセージ
// リリースモードの dart compile は断言を除去
// 不変条件の検査向け、ユーザー入力には不向き

12.ファイルと I/O

ファイルの読み書き、JSON、標準入出力。

ファイルの読み込み

dart:io の File クラスでファイルを読み書きします。readAsString でテキストファイル全体を読み込みます。import 'dart:io' が必要です。

1
2
3
4
5
6
7
8
9
10
11
import 'dart:io';
Future<void> main() async {
final text = await File('data.txt').readAsString();
print(text);
final exists = await File('data.txt').exists();
print(exists);
}
// readAsString は一度にメモリへ読み込む
// 大きなファイルはストリーム読み込み
// パスが存在しないと FileSystemException
// exists は存在有無を真偽で返す

ファイルの書き込み

writeAsString でテキスト書き込み、writeAsBytes でバイナリ書き込み。追記は mode パラメータで指定します。

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
Future<void> main() async {
await File('out.txt').writeAsString('Hello\n');
await File('out.txt').writeAsString('World\n',
mode: FileMode.append); // 追加
final content = await File('out.txt').readAsString();
print(content);
}
// 既定モードは元ファイルを上書き
// FileMode.append は末尾に追記
// FileMode.writeOnly は書き込み専用で上書き
// 中国語の書き込みは既定で UTF-8 エンコーディング

行ごとの読み込み

readAsLines はファイルを行ごとにリストへ分割します。大ファイルは openRead でストリーム処理し、全体読み込みを避けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final lines = await File('log.txt').readAsLines();
for (final line in lines) {
print(line);
}
final stream = File('big.log').openRead()
.transform(utf8.decoder)
.transform(const LineSplitter());
await for (final line in stream) {
print(line);
}
}
// readAsLines は小さなファイル向け
// 大きなファイルは openRead でストリーム処理
// LineSplitter は行ごとに分割
// ストリーム処理はメモリを占有しない

JSON 処理

dart:convert の jsonEncode / jsonDecode でシリアライズとデシリアライズを行います。JSON の数値は int か double になります。

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:convert';
void main() {
final data = {'name': 'Rex', 'age': 5};
final encoded = jsonEncode(data);
print(encoded);
final decoded = jsonDecode(encoded) as Map<String, dynamic>;
print(decoded['name']);
}
// jsonDecode は dynamic を返す、変換が必要
// 数値は int か double に解析
// 未知のフィールドは Map に残る
// 複雑なオブジェクトは手動でマッピング

標準入出力

stdin.readLineSync で 1 行同期読み込み、stdout.writeln で出力します。コマンドライン対話プログラムに向きます。

1
2
3
4
5
6
7
8
9
10
import 'dart:io';
void main() {
stdout.write('请输入姓名: ');
final name = stdin.readLineSync() ?? '';
stdout.writeln('你好,$name');
}
// readLineSync は 1 行読んで String? を返す
// 入力終了で null、デフォルト値が必要
// stdout.write は改行しない
// stderr.writeln はエラーストリームへ出力

ディレクトリの操作

Directory.list でディレクトリ内容を列挙し、create で作成、delete で削除します。返値は FileSystemEntity です。

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
Future<void> main() async {
await Directory('data/sub').create(recursive: true);
await for (final entity in Directory('.').list()) {
print('${entity.path} ${entity is Directory ? '目录' : '文件'}');
}
await Directory('data').delete(recursive: true);
}
// create recursive は多段ディレクトリを作成
// list はディレクトリ内容を遅延反復
// delete recursive は再帰削除
// entity でファイルとディレクトリを統一的に扱う

バイナリバイト

readAsBytes でバイナリを読み、Uint8List でバイト列を表現し、writeAsBytes で書き出します。画像や音声に適します。

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:io';
import 'dart:typed_data';
Future<void> main() async {
final bytes = await File('image.png').readAsBytes();
print('大小: ${bytes.length} 字节');
final copy = Uint8List.fromList(bytes);
await File('copy.png').writeAsBytes(copy);
print('已复制');
}
// readAsBytes は Uint8List を返す
// バイトは非テキストデータに適する
// fromList でバイト列をコピー
// 大きなファイルは openWrite でストリーム書き込み

パスの操作

File / Directory の path プロパティでフルパスを取得できます。absolute は絶対パスへ、uri は file:// 形式に変換します。

1
2
3
4
5
6
7
8
9
10
11
12
import 'dart:io';
void main() {
final file = File('a/b/data.txt');
print(file.path); // 完整路径
print(file.absolute.path); // 绝对路径
print(file.uri); // file:// URI
print(Platform.pathSeparator);
}
// path プロパティは完全なパス文字列を返す
// absolute は絶対パスに解決
// uri は file:// URI に変換
// Windows と Unix で区切り文字が異なる

13.よくある落とし穴

Dart の日常開発で最も踏みがちな落とし穴と正しい書き方。

null 表明の乱用

! で null チェックを迂回すると実行時にクラッシュしがちです。null チェックや ??、型昇格のほうが安全です。

1
2
3
4
5
6
7
8
9
10
11
// BAD:用 ! 掩盖可空设计
String? name = 'Rex';
final len = name!.length; // 依赖运行时断言
print(len);
// GOOD:利用类型提升
String? nick = '小雷';
if (nick != null) {
print(nick.length);
}
// GOOD:?? 提供默认值
print(name?.toUpperCase() ?? '未知');

コレクションの == 比較

List / Map の == は参照比較で、要素が同じでも等価にはなりません。構造比較はヘルパー関数を使います。

1
2
3
4
5
6
7
8
9
// BAD:== 比较的是引用
final a = [1, 2];
final b = [1, 2];
print(a == b); // false
// GOOD:比较内容而非引用
print(a.join(',') == b.join(',')); // true
// GOOD:结构比较推荐
// listEquals(a, b) package:collection
// 字符串与数字直接 == 比较内容

const と final

const はコンパイル時定数でなければならず、final はランタイムで決定します。ランタイム値を const で書くとコンパイルエラーです。

1
2
3
4
5
6
7
8
9
// BAD:运行时值不能作 const
// const now = DateTime.now(); // 编译错误
// GOOD:运行时值用 final
final now = DateTime.now();
// GOOD:编译期常量用 const
const pi = 3.14159;
// const 值会规范化共享
// final 只是不能重新赋值
// 集合元素也可用 const

遅延 Iterable

map / where は lazy Iterable を返し、反復時に再計算されるため元集合の変更が結果に反映されます。スナップショットは toList を使います。

1
2
3
4
5
6
7
8
9
10
11
// BAD:惰性 Iterable 每次遍历重新计算
final nums = [1, 2, 3];
final lazy = nums.where((n) => n.isOdd);
nums.add(5);
print(lazy.toList()); // [1, 3, 5] 含新元素
// GOOD:需要快照用 toList
final snapshot = nums.where((n) => n.isOdd).toList();
nums.add(7);
print(snapshot); // 仍是 [1, 3, 5]
// 底层变化不影响已固化结果
// 流(Stream)只能监听一次

await の書き忘れ

async 関数を await しないと Future が返り、結果ではありません。並列実行は Future.wait を使います。

1
2
3
4
5
6
7
8
9
10
11
12
// BAD:await を忘れると Future オブジェクトになる
Future<String> fetch() async => '数据';
Future<void> main() async {
final result = fetch(); // Future は文字列ではない
print(result);
// GOOD:await して初めて結果になる
final text = await fetch();
print(text);
// 複数を並行して待つ:
final all = await Future.wait([fetch(), fetch()]);
print(all);
}

カスケードはレシーバを返す

.. カスケードは同じオブジェクトに連続呼び出しを行い、レシーバを返します。戻り値が必要なときはカスケードを使わないでください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User {
String name = '';
int age = 0;
}
void main() {
// BAD:カスケードが返すのはレシーバー
final result = [1, 2]..add(3);
print(result); // 3 でなく [1, 2, 3]
// GOOD:カスケードはその場で変更し元のオブジェクトを返す
final user = User()
..name = 'Rex'
..age = 5;
print(user.name);
// 関数の結果が要るなら通常の呼び出し
print('abc'.toUpperCase());
}

反復中の変更

反復中に要素を追加・削除すると ConcurrentModificationError が投げられます。removeWhere や収集してから処理しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD:走査中に要素を削除すると例外
final items = [1, 2, 3, 4];
// for (final n in items) { items.remove(n); }
// 抛 ConcurrentModificationError
// GOOD:removeWhere で安全にフィルタ
final nums = [1, 2, 3, 4];
nums.removeWhere((n) => n.isEven);
print(nums); // [1, 3]
// GOOD:先に集めてから一括処理
final keep = <int>[];
for (final n in nums) {
if (n.isOdd) keep.add(n);
}
print(keep);

文字列はエンコード単位

length は UTF-16 のコードユニット数を返すため、漢字や emoji は文字数より多くなります。文字単位で処理するには runes を使います。

1
2
3
4
5
6
7
8
9
10
11
12
// BAD:length で中国語の文字を数える
final s = '你好';
print(s.length); // 2 个单元
// emoji の場合:
final emoji = '👍';
print(emoji.length); // 2!一つの記号
// GOOD:runes でコードポイントを数える
print(emoji.runes.length); // 1
// GOOD:runes で文字ごとに処理
for (final r in emoji.runes) {
print(String.fromCharCode(r));
}

14.並行と非同期

isolate、Future、Stream、イベントループ。

isolate 並行

isolate は Dart の並行単位で、それぞれ独立したメモリとイベントループを持ちます。Isolate.run か Isolate.spawn で起動します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:isolate';
Future<void> main() async {
final result = await Isolate.run(() {
var sum = 0;
for (var i = 0; i < 100000; i++) {
sum += i;
}
return sum;
});
print('结果: $result');
}
// Isolate.run は Future<結果> を返す
// メモリを分離、可変状態を共有しない
// CPU 集約的な計算に適する
// 計算完了で自動的に破棄

spawn とポート

Isolate.spawn はエントリ関数で isolate を起動します。SendPort が送信、ReceivePort が受信を担当します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:isolate';
void entry(SendPort sendPort) {
sendPort.send('来自新 isolate');
}
Future<void> main() async {
final receive = ReceivePort();
await Isolate.spawn(entry, receive.sendPort);
final msg = await receive.first;
print(msg);
receive.close();
}
// spawn の入口はトップレベル関数か静的メソッドが必要
// メッセージはポート経由で渡す
// receive.first で最初のメッセージを待つ
// 使い終わったらポートを close

Future の基本

Future は将来得られる結果を表します。Future.value で即座に成功し、await で完了を待ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Future<int> addLater(int a, int b) async {
await Future.delayed(Duration(milliseconds: 10));
return a + b;
}
Future<void> main() async {
final future = Future.value(42);
print(await future);
final result = await addLater(1, 2);
print(result);
}
// async 関数は Future を返す
// await は Future 完了まで待機
// Future.delayed はタイマー遅延
// Future.value は即時の結果を包む

async / await

async は非同期関数を表し、await は非同期操作を待ちます。await は async 関数内でのみ使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<String> fetchUser() async {
await Future.delayed(Duration(milliseconds: 50));
return 'Rex';
}
Future<void> main() async {
print('开始加载');
final name = await fetchUser();
print('加载到: $name');
final results = await Future.wait([
fetchUser(),
fetchUser(),
]);
print(results);
}
// await は現在の流れを止めるがメインスレッドはブロックしない
// 複数の await は既定で直列
// Future.wait は複数を並行して待つ
// 例外は await の場所に伝わり捕捉できる

Stream

Stream は非同期イベントのシーケンスです。listen で購読し、await for で逐次処理します。Future の単発値とは異なります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:async';
Stream<int> tick(int max) async* {
for (var i = 0; i < max; i++) {
await Future.delayed(Duration(milliseconds: 10));
yield i;
}
}
Future<void> main() async {
await for (final n in tick(3)) {
print('tick $n');
}
tick(2).listen((n) => print('listen $n'));
await Future.delayed(Duration(milliseconds: 100));
}
// await for はストリームのイベントを順に消費
// listen でリスナーを登録
// ストリームは一度しか生成されず再生できない
// 各イベントは順に到達

StreamController

StreamController で手動制御し、add でデータ、addError でエラー、close で終了します。broadcast で複数購読できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:async';
void main() {
final controller = StreamController<int>();
controller.stream.listen((n) => print('收到 $n'));
controller.add(1);
controller.add(2);
controller.close();
final broadcast = StreamController<int>.broadcast();
broadcast.stream.listen((n) => print('A $n'));
broadcast.stream.listen((n) => print('B $n'));
broadcast.add(5);
broadcast.close();
}
// 既定のストリームはリスナー 1 つだけ
// broadcast は複数リスナーを許可
// add でストリームへデータを送る
// close を忘れずリソース解放

複数の Future を待つ

Future.wait で全完了を並行待ち、Future.any で最も速い結果を待ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Future<int> slow() async {
await Future.delayed(Duration(milliseconds: 30));
return 1;
}
Future<int> fast() async {
await Future.delayed(Duration(milliseconds: 10));
return 2;
}
Future<void> main() async {
final all = await Future.wait([slow(), fast()]);
print(all); // [1, 2]
final first = await Future.any([slow(), fast()]);
print(first); // 2
}
// wait は全部成功で結果リストを返す
// どれか失敗すると既定でエラー伝播
// any は最初に完了したものを返す
// eagerError で即時失敗を制御

Completer

Completer を使うと Future の完了タイミングを手動で制御でき、コールバック API を async/await にラップできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:async';
Future<String> fromCallback() {
final completer = Completer<String>();
Future.delayed(Duration(milliseconds: 20), () {
completer.complete('回调结果');
});
return completer.future;
}
Future<void> main() async {
final result = await fromCallback();
print(result);
}
// complete で Future を成功させる
// completeError で Future を失敗させる
// complete できるのは一度だけ
// サードパーティのコールバックを包むのに使う

15.ネットワーク

HTTP リクエスト、WebSocket、TCP ソケット。

HTTP クライアント

dart:io の HttpClient で HTTP リクエストを発行します。getUrl でリクエストを取得し、close してからボディを読みます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final client = HttpClient();
final request = await client.getUrl(Uri.parse('https://example.com'));
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
print('状态码: ${response.statusCode}');
print(body.substring(0, 50));
client.close();
}
// HttpClient は低レベルで手動 close が必要
// レスポンス本体はバイトストリーム、utf8.decoder を使う
// statusCode 200 は成功を表す
// 単純な用途では package:http を優先

package:http

package:http は一般的なリクエストをラップします。http.get / http.post は Response を返し、body はそのまま文字列です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://example.com'),
);
print('状态码: ${response.statusCode}');
print(response.body.substring(0, 50));
final post = await http.post(
Uri.parse('https://example.com/api'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'name': 'Rex'}),
);
print(post.statusCode);
}
// pubspec に http 依存を追加する必要がある
// Response.body は既にデコード済み文字列
// headers にリクエストヘッダを渡す
// ステータスコードで成功かを判断

URI のパース

Uri.parse で URL を解析し、queryParameters でクエリを読み、Uri.http でリクエスト URL を構築します。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final uri = Uri.parse('https://api.com/users?id=5&page=2');
print(uri.scheme); // https
print(uri.host); // api.com
print(uri.path); // /users
print(uri.queryParameters); // {id: 5, page: 2}
final built = Uri.http('api.com', '/users', {'id': '5'});
print(built);
}
// queryParameters は自動デコード
// Uri.https は TLS を使う
// queryParametersAll は重複キーを処理
// replace で新しい URI を派生

JSON API

JSON API を呼び出し、jsonDecode でレスポンスボディを解析します。package:http と Future を組み合わせて待ちます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List;
for (final item in data) {
final user = item as Map<String, dynamic>;
print(user['name']);
}
}
}
// ステータスコードを確認してから解析
// JSON 配列は List、オブジェクトは Map に
// 存在しないフィールドは null を返す
// 本番ではモデルクラスでラップできる

WebSocket

WebSocket は全二重通信です。WebSocket.connect で接続し、add で送信、ストリームで受信します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final ws = await WebSocket.connect('wss://example.com/ws');
ws.add('你好');
await for (final message in ws) {
print('收到: $message');
if (message == 'bye') {
await ws.close();
}
}
}
// connect で接続を確立
// add でメッセージ送信
// ストリームで応答メッセージを受信
// close で graceful に閉じる

TCP ソケット

Socket.connect で TCP 接続し、リクエストを書き込み、レスポンスを読みます。低レベルプロトコルや自作サービスに向きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
final socket = await Socket.connect('example.com', 80);
socket.write('GET / HTTP/1.0\r\n\r\n');
await for (final data in socket) {
final text = String.fromCharCodes(data);
if (text.contains('HTTP')) print(text);
}
socket.destroy();
}
// Socket はバイトストリームを提供
// write でリクエスト内容を送信
// レスポンスの分割をストリームで読む
// destroy で即時クローズ

HTTP サーバー

HttpServer.bind でローカルサーバーを起動し、リクエストを待ち受けてレスポンスを返します。開発やツールに適しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
final server = await HttpServer.bind('localhost', 8080);
print('监听 8080 端口');
await for (final request in server) {
request.response.headers.set('Content-Type', 'text/plain; charset=utf-8');
request.response.write('你好,路径 ${request.uri.path}');
await request.response.close();
}
}
// bind でアドレスとポートをバインド
// 各リクエストは await for に入る
// response オブジェクトで内容を返す
// close で今回のレスポンスを終了

タイムアウトとエラー

ネットワークリクエストはタイムアウトや失敗が起こり得ます。.timeout を設定し、try/catch で例外を捕捉して処理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'dart:async';
import 'package:http/http.dart' as http;
Future<void> main() async {
try {
final response = await http
.get(Uri.parse('https://example.com'))
.timeout(const Duration(seconds: 5));
print(response.statusCode);
} on TimeoutException {
print('请求超时');
} catch (e) {
print('网络错误: $e');
}
}
// timeout は時間超過で TimeoutException を投げる
// ネットワーク例外は多くが SocketException
// エラーの型ごとに分けて処理
// 本番では指数バックオフ再試行も追加

16.日時

DateTime、Duration、フォーマット、タイムスタンプ。

現在時刻

DateTime.now() で現在のローカル時刻を取得します。各フィールドに直接アクセスでき、ミリ秒も取得できます。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final now = DateTime.now();
print('${now.year}-${now.month}-${now.day}');
print('${now.hour}:${now.minute}:${now.second}');
print(now.weekday); // 周一=1 周日=7
print(now.millisecondsSinceEpoch);
}
// now はローカルタイムゾーンの時間
// month/day は 1 から始まる
// weekday は 1-7、月曜が 1
// 各フィールドは int

時刻の生成

DateTime(年, 月, 日, ...) で指定時刻を作ります。DateTime.utc で UTC 時刻を構築します。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final birthday = DateTime(2020, 1, 15);
print(birthday);
final launch = DateTime(2026, 8, 2, 10, 30);
print(launch);
final utc = DateTime.utc(2026, 1, 1);
print(utc.isUtc); // true
}
// 渡さない部分は既定で 0 か 1
// 不正な月日は自動で繰り上げ
// UTC の生成は DateTime.utc
// 比較は isBefore/isAfter

Duration

Duration は時間の長さを表し、時・分・秒・マイクロ秒をサポートします。算術、比較、単位変換が可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final halfHour = Duration(minutes: 30);
final fiveMin = Duration(minutes: 5);
print(halfHour.inSeconds); // 1800
print(halfHour + fiveMin); // 35 分钟
print(halfHour > fiveMin); // true
final d = Duration(days: 1, hours: 2);
print('${d.inHours} 小时'); // 26
}
// inSeconds/inMinutes などで単位変換
// + - * / の演算に対応
// 大小比較ができる
// 遅延には Duration(milliseconds: n) が定番

フォーマット出力

Dart には組み込みの strftime がなく、padLeft でゼロパディングするか、intl パッケージの DateFormat を使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
final now = DateTime.now();
final y = now.year.toString();
final m = now.month.toString().padLeft(2, '0');
final d = now.day.toString().padLeft(2, '0');
print('$y-$m-$d');
print(now.toIso8601String());
// intl パッケージが日付テンプレートを提供:
// import 'package:intl/intl.dart';
// print(DateFormat('yyyy-MM-dd').format(now));
}
// padLeft で桁を埋める
// toIso8601String は ISO 形式を出力
// intl はローカライズされた日付テンプレート対応
// 単純な用途は手書きの連結で十分

時刻のパース

DateTime.parse で ISO 8601 文字列を解析し、toIso8601String で標準形式の文字列を生成して送受信しやすくします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final parsed = DateTime.parse('2026-08-02 10:30:00');
print(parsed.year); // 2026
final iso = DateTime.parse('2026-08-02T10:30:00Z');
print(iso.isUtc); // true
final parts = '2026-08-02'.split('-');
final d = DateTime(
int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]));
print(d.day);
}
// さまざまな ISO 形式に対応
// タイムゾーンなしはローカル時間
// Z を含むと UTC
// 不正な形式は FormatException

時刻演算

add / subtract で Duration を加減し、difference で 2 つの時刻差を Duration で求めます。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final start = DateTime(2026, 8, 2, 10);
final later = start.add(Duration(hours: 5));
print(later.hour); // 15
final earlier = start.subtract(const Duration(days: 1));
print(earlier.day); // 1
final diff = later.difference(start);
print(diff.inHours); // 5
}
// add/subtract は新しいオブジェクトを返す
// 元のオブジェクトは不変
// difference は常に非負
// 方向付きの判断は isBefore を使う

タイムスタンプ

ミリ秒/マイクロ秒タイムスタンプは保存や並べ替えに便利です。fromMillisecondsSinceEpoch で時刻に復元します。

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
final now = DateTime.now();
final ms = now.millisecondsSinceEpoch;
print(ms);
final back = DateTime.fromMillisecondsSinceEpoch(ms);
print(back.toIso8601String());
final day = DateTime(now.year, now.month, now.day);
print(day.millisecondsSinceEpoch);
}
// タイムスタンプは 1970-01-01 からのミリ秒数
// 保存は UTC タイムスタンプで時区の混乱を回避
// fromMicrosecondsSinceEpoch はマイクロ秒対応
// 同じタイムスタンプから復元した時間は等しい

タイムゾーン

DateTime は既定でローカル時刻です。isUtc で判定し、toUtc / toLocal で変換します。保存は UTC が推奨されます。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final now = DateTime.now();
print(now.isUtc); // false
final utc = now.toUtc();
print(utc.isUtc); // true
final local = utc.toLocal();
print(local == now); // 时间一致
}
// toUtc/toLocal の相互変換は時刻を変えない
// DateTime にタイムゾーン名は内蔵されない
// IANA タイムゾーンが必要なら package:timezone
// 永続化は UTC 保存を推奨

ストップウォッチ

Stopwatch はコードの実行時間を計測します。start / stop / reset で制御し、elapsed で経過時間を取得します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final watch = Stopwatch()..start();
var sum = 0;
for (var i = 0; i < 1000000; i++) {
sum += i;
}
watch.stop();
print('耗时 ${watch.elapsedMilliseconds} ms');
print('微秒 ${watch.elapsedMicroseconds}');
}
// start で計時開始
// elapsed は Duration を返す
// elapsedMilliseconds はミリ秒数
// reset でゼロに戻して再開

17.プロセスと環境

サブプロセス、環境変数、標準ストリーム、シグナル。

子プロセスの実行

Process.run は外部コマンドを実行して終了を待ちます。result.stdout / stderr が出力を保持します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final result = await Process.run('dart', ['--version']);
print('退出码: ${result.exitCode}');
print('输出: ${result.stdout}');
final ls = await Process.run(
Platform.isWindows ? 'cmd' : 'ls',
Platform.isWindows ? ['/c', 'dir'] : ['-la'],
);
print(ls.stdout);
}
// run はプロセス終了を待って結果を返す
// stdout/stderr は文字列かバイト
// exitCode 0 は成功を表す
// 対話が必要なら Process.start

ストリーミングプロセス

Process.start はプロセスを起動して即座に返し、stdout のリアルタイム読み取りと stdin への書き込みができます。長時間の処理に向きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final process = await Process.start('dart', ['--version']);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) => print('子进程输出: ${line.trim()}'));
final exitCode = await process.exitCode;
print('退出码: $exitCode');
}
// start はプロセス終了を待たない
// stdout はストリームでリアルタイム監視
// 対話が必要なら stdin に書き込む
// environment で環境をカスタマイズ

環境変数

Platform.environment で環境変数を読み取ります(読み取り専用 Map)。存在しないキーは null です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import 'dart:io';
void main() {
final env = Platform.environment;
final path = env['PATH'] ?? '未设置';
print('PATH: $path');
print('有 HOME: ${env.containsKey('HOME')}');
for (final entry in env.entries) {
if (entry.key.startsWith('FLUTTER')) {
print('${entry.key}=${entry.value}');
}
}
}
// environment は読み取り専用の Map
// 存在しないキーは null
// containsKey で存在を判断
// プロセス生成時に environment を渡して注入可

終了コード

exitCode で終了コードを設定します。0 が成功、非 0 が失敗で、シェルが結果を判定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
void main(List<String> args) {
if (args.isEmpty) {
stderr.writeln('用法: app <参数>');
exitCode = 1;
return;
}
print('处理 ${args[0]}');
exitCode = 0; // 成功
}
// exitCode 0 は成功
// 0 以外はエラーの種類
// exit() はプロセスを即終了
// スクリプトは終了コードで結果を判断

引数とスクリプト

args はコマンドライン引数、Platform.script はエントリスクリプトのパス、Directory.current は現在の作業ディレクトリです。

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:io';
void main(List<String> args) {
print('参数个数: ${args.length}');
for (var i = 0; i < args.length; i++) {
print('参数 $i: ${args[i]}');
}
print('脚本路径: ${Platform.script}');
print('当前目录: ${Directory.current.path}');
}
// args にプログラム名は含まれない
// Platform.script は入口ファイルのパス
// Directory.current は現在の作業ディレクトリ
// 複雑な引数は args パッケージで解析

標準ストリーム

stdout / stderr が出力、stdin が入力です。flush でバッファを強制出力し、stderr は通常のパイプ出力に含まれません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
void main() {
stdout.write('请输入:');
stdout.flush();
final input = stdin.readLineSync() ?? '';
stdout.writeln('你输入了: $input');
if (input.isEmpty) {
stderr.writeln('警告:输入为空');
}
}
// stdout.write は改行しない
// flush はバッファを即時出力
// stderr はエラーストリームへ
// パイプ処理では stdout がリダイレクトされる

ファイルシステム

File / Directory がファイルシステムを扱います。create で作成、delete で削除、rename で移動、exists で存在判定です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import 'dart:io';
Future<void> main() async {
final dir = Directory('data/sub');
await dir.create(recursive: true);
final file = File('data/note.txt');
await file.writeAsString('内容');
print(await file.exists()); // true
print(file.lengthSync()); // 字节数
await file.rename('data/new.txt');
await Directory('data').delete(recursive: true);
}
// create recursive で多段ディレクトリを作成
// exists は存在有無を判断
// lengthSync は同期でサイズ取得
// rename は移動または改名

シグナル処理

ProcessSignal でシステムシグナルに応答します(SIGINT / SIGTERM)。優雅な終了や後処理に使います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:io';
Future<void> main() async {
ProcessSignal.sigint.watch().listen((_) {
print('收到中断,清理退出');
exit(0);
});
print('运行中,按 Ctrl+C 退出');
await Future.delayed(Duration(seconds: 30));
}
// sigint は Ctrl+C
// sigterm は kill のデフォルトシグナル
// watch はシグナルを流で監視
// 処理後に exit を呼んで終了
// Windows は一部のシグナルのみ使える

18.正規表現

RegExp によるマッチ、置換、分割、グループ。

作成と判定

RegExp は正規表現を表します。hasMatch でマッチ有無を判定します。r プレフィックスでエスケープを避けられます。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final digits = RegExp(r'\d+');
print(digits.hasMatch('abc123')); // true
print(digits.hasMatch('abc')); // false
final email = RegExp(r'^[\w.-]+@[\w-]+\.\w+$');
print(email.hasMatch('[email protected]'));
}
// r プレフィックスは生文字列を表す
// \d 数字 \w 単語文字 \s 空白
// ^ 行頭 $ 行末
// hasMatch は存在するかだけ判断

最初のマッチ

firstMatch は最初のマッチを返します。group でグループを、start / end で位置を取得し、マッチが無いと null です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final re = RegExp(r'\d+');
final match = re.firstMatch('编号 42,编号 99');
if (match != null) {
print(match.group(0)); // 42
print(match.start); // マッチの開始位置
print(match.end); // マッチの終了位置
print(match.matched); // 42
}
}
// firstMatch は最初のマッチを返す
// マッチがなければ null
// group(0) はマッチ全体
// group(n) は n 番目のグループ

すべてのマッチ

allMatches はすべてのマッチの反復子を返します。パターンと組み合わせて条件に合う内容を抽出できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void main() {
final re = RegExp(r'\d+');
final matches = re.allMatches('a1 b22 c333');
final numbers = <String>[];
for (final m in matches) {
numbers.add(m.group(0)!);
}
print(numbers); // [1, 22, 333]
final list = RegExp(r'\d+')
.allMatches('a1 b22')
.map((m) => m.group(0)!)
.toList();
print(list);
}
// 各マッチオブジェクトを走査
// group(0) でマッチ全体を取得
// map と組み合わせると簡潔
// 遅延反復で必要なときに計算

置換

replaceAll は全置換、replaceFirst は最初の 1 つだけ置換します。コールバックで動的に置換文字列を生成できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final re = RegExp(r'\d+');
print(re.replaceAll('a1b2', '#')); // a#b#
print(re.replaceFirst('a1b2', '#')); // a#b2
final result = re.replaceAllMapped('a1b2', (m) {
final n = int.parse(m.group(0)!);
return '${n + 1}';
});
print(result); // a2b3
}
// replaceAll はすべて置換
// replaceFirst は最初のみ
// コールバックで内容に応じて計算
// 元の文字列は不変、新しい文字列を返す

分割

String.split は RegExp を受け取りパターンで分割します。区切り文字は結果に含まれません。

1
2
3
4
5
6
7
8
9
10
11
void main() {
final text = 'a, b, c';
final parts = text.split(RegExp(r',\s*'));
print(parts); // [a, b, c]
print('one two three'.split(RegExp(r'\s+')));
print(text.replaceAll(RegExp(r',\s*'), ' | '));
}
// split はマッチした位置で分割
// 区切り文字は結果に含まれない
// 複雑な区切りは正規表現
// 単純な区切りは文字列を直接渡す

キャプチャグループ

丸括弧はキャプチャグループを定義します。group(n) で取得、(?<name>...) で名前付きにして namedGroup で参照します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main() {
final re = RegExp(r'(\d{4})-(\d{2})-(\d{2})');
final m = re.firstMatch('日期 2026-08-02');
if (m != null) {
print(m.group(1)); // 2026
print(m.group(2)); // 08
print(m.group(3)); // 02
}
final named = RegExp(r'(?<year>\d{4})');
final nm = named.firstMatch('2026');
print(nm?.namedGroup('year'));
}
// group(1) 以降はキャプチャグループ
// 非キャプチャ (?:...) は番号を消費しない
// 名前付きグループで可読性向上
// namedGroup で名前で取得

フラグ

RegExp コンストラクタの第 2 引数でフラグを設定します。caseSensitive、multiLine、dotAll、unicode などがあります。

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
final re = RegExp('dart', caseSensitive: false);
print(re.hasMatch('Hello DART')); // true
final multiline = RegExp('^a', multiLine: true);
print(multiline.hasMatch('x\na\nb')); // true
final dotAll = RegExp('a.b', dotAll: true);
print(dotAll.hasMatch('a\nb')); // true
}
// caseSensitive: false で大文字小文字を無視
// multiLine は ^ を行頭すべてにマッチ
// dotAll は . を改行にもマッチ
// unicode: true で Unicode モード有効

よくあるパターン

よくある検証パターンとしてメール、電話番号、URL、IP を再利用可能な定数として定義すると保守しやすくなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() {
final email = RegExp(r'^[\w.+-]+@[\w-]+\.\w+$');
final phone = RegExp(r'^1[3-9]\d{9}$');
final url = RegExp(r'^https?://\S+$');
final ip = RegExp(r'^\d{1,3}(\.\d{1,3}){3}$');
print(email.hasMatch('[email protected]'));
print(phone.hasMatch('13800138000'));
print(url.hasMatch('https://dart.dev'));
print(ip.hasMatch('192.168.1.1'));
}
// メール:一般的な検証
// 電話番号:中国の 11 桁
// URL:http/https
// IP:4 セグメントのドット区切り十進数

19.ビルドとデバッグ

pub、静的解析、フォーマッタ、テスト。

pubspec の設定

pubspec.yaml はパッケージ設定ファイルで、名前、バージョン、SDK 制約、依存関係を宣言します。dart pub get で解決します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// pubspec.yaml の例:
// name: myapp
// description: 示例应用
// version: 1.0.0
// environment:
// sdk: '>=3.0.0 <4.0.0'
// dependencies:
// http: ^1.1.0
// dev_dependencies:
// test: ^1.24.0
// バージョン制約:
// ^1.1.0 は >=1.1.0 <2.0.0 を表す
// 依存のインストール:
// dart pub get

pub コマンド

dart pub get で取得、upgrade で更新、outdated で期限切れ確認、add / remove で依存関係を管理します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 依存のインストール:
// dart pub get
// すべての依存をアップグレード:
// dart pub upgrade
// 期限切れ依存の確認:
// dart pub outdated
// 依存の追加:
// dart pub add http
// 依存の削除:
// dart pub remove http
// パッケージの検索:
// dart pub search json
// 公開:
// dart pub publish
// バージョンは pubspec.lock に固定

静的解析

dart analyze でコードを静的に解析し、エラーや警告を報告します。CI で実行して品質を担保します。

1
2
3
4
5
6
7
8
9
10
11
12
// 現在のパッケージを解析:
// dart analyze
// 指定ファイルのみ解析:
// dart analyze lib/main.dart
// よくある検査項目:
// 未使用の import
// 型エラー
// 非推奨の書き方
// lint 修正を適用:
// dart fix --apply
// CI 統合:
// dart analyze && dart test

フォーマット

dart format でコードスタイルを統一します。インデント、引用符、改行を自動整形し、チーム開発に必須です。

1
2
3
4
5
6
7
8
9
10
11
// 現在のディレクトリを整形:
// dart format .
// 指定ファイルを整形:
// dart format lib/main.dart
// 変更せずに検査:
// dart format --output=none --set-exit-if-changed .
// スタイルの慣例:
// 2 スペースのインデント
// シングルクォート優先
// 行末に空白を置かない
// CI で整形の一貫性を検査

ユニットテスト

package:test が test() と expect を提供します。dart test で実行し、テストは test/ ディレクトリに置きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// test/calculator_test.dart:
import 'package:test/test.dart';
int add(int a, int b) => a + b;
void main() {
test('加法正确', () {
expect(add(1, 2), 3);
expect(add(-1, 1), 0);
});
group('边界情况', () {
test('大数相加', () {
expect(add(1000000, 1), 1000001);
});
});
}
// テストの実行:
// dart test
// 単一ファイルのみ実行:
// dart test test/calculator_test.dart
// 名前でフィルタ:
// dart test -n 加法

コンパイルとデプロイ

dart compile で実行ファイルを生成します。exe はネイティブ、js は JavaScript、aot-snapshot は AOT スナップショットです。

1
2
3
4
5
6
7
8
9
10
11
12
// ネイティブ実行ファイルにコンパイル:
// dart compile exe bin/main.dart
// 出力を指定:
// dart compile exe bin/main.dart -o myapp
// JS(ブラウザ)にコンパイル:
// dart compile js lib/main.dart
// AOT スナップショット:
// dart compile aot-snapshot bin/main.dart
// コンパイル核:
// dart compile kernel bin/main.dart
// コンパイル後は Dart SDK なしで実行可
// ネイティブ exe は起動が速い

lint ルール

analysis_options.yaml で lint ルールを設定します。lints パッケージは推奨ルールセットを提供します。

1
2
3
4
5
6
7
8
9
10
11
12
13
// analysis_options.yaml の例:
// include: package:lints/recommended.yaml
// linter:
// rules:
// - prefer_final_locals
// - avoid_print
// analyzer:
// language:
// strict-casts: true
// 推奨ルールセット:
// package:lints/recommended.yaml
// Flutter プロジェクトは flutter_lints を使う
// dart analyze がこれらのルールを適用

デバッグ

print デバッグが最も手軽です。assert で不変条件をチェックし、IDE のブレークポイントでステップ実行もできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
final total = calculate(3, 4);
print('调试: total=$total');
assert(total > 0, '结果应为正');
print(total);
}
int calculate(int a, int b) {
final sum = a + b;
// dart:developer の log はレベル指定に対応:
// log('中间值 $sum', name: 'calc');
return sum;
}
// print はコンソールへ出力
// dart:developer の log はレベル指定に対応
// アサーションはリリースモードで除去
// ブレークポイントでステップ実行を支援
// Flutter の大量ログには debugPrint を使う

公式リンク

公式ドキュメントとリソースへの直接リンク。

このチートシートについて

本ページは Dart 3 の自己完結的なチートシートで、言語コアと最もよく使う標準ライブラリを実プロジェクトの約 80% の用途でカバーします。モダンなイディオムを中心に据えています — 健全な null 安全、レコードとパターン、switch 式、sealed クラス、カスケード演算子、そして isolate ベースの並行処理。Dart は Google が設計し、Flutter の基盤言語です。JIT による高速な開発と、AOT による効率的なリリースの両方をサポートしています。正式なリファレンスは Dart 言語ツアーと Effective Dart を参照してください。 19 章はそれぞれ一つのテーマに取り組みます — 最初のプログラムから isolate、よくある落とし穴、ビルド・テストまで。各章は 8 個のサンプル(各 5〜15 行)に分かれており、合計でおよそ 152 トピックです。スニペットは短く自己完結的にしてあります。コメントは中国語のまま残し、学習時の対照に役立ててください。 処理はすべてブラウザ内で完結します — アップロードや追跡は一切ありません。本ページは GuruToolkit の無料開発者ツールキットの一部で、スニペットは保証なしで自由にご利用いただけます。

バージョン 2.1.0