Dart 関数 — 第一級市民とクロージャ
関数はコードの構成要素である — 小さな関数が組み合わさって大きなシステムを構築する。
1. 学べること
- 関数宣言とアロー関数(
=>構文) - オプショナルパラメータ:名前付きパラメータ
{}と位置指定パラメータ[] requiredパラメータマーカー- クロージャと変数キャプチャのメカニズム
- 高階関数:forEach / map / where / reduce / fold
2. 開発者のリアルな物語
(1) 課題:パラメータ混同による呼び出しエラー
Bob は DataPipeline に 7 個の位置指定パラメータを持つ processOrder 関数を定義した。チームメンバーは呼び出し時にパラメータの順序をよく間違え、誰かが taxRate を discountRate として渡し、バッチ内の全 5 万件の注文で誤った割引計算が行われた。顧客の苦情が急増し、緊急対応に 2 日を費やした。
(2) 解決策:名前付きパラメータ
Dart の名前付きパラメータ {} は各パラメータに明確なラベルを与え、呼び出し時の順序依存を排除する。コンパイラは required パラメータが提供されているかもチェックできる。
// 修正前: 位置指定パラメータ - 混同しやすい
// processOrder('ORD-001', 1500.0, 0.08, 0.1, true, 'USD', 'json');
// 修正後: 名前付きパラメータ - 自己文書化
processOrder(
id: 'ORD-001',
amount: 1500.0,
taxRate: 0.08,
discountRate: 0.1,
currency: 'USD',
);
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
(3) 効果
- 名前付きパラメータにより関数呼び出しが自己文書化され、パラメータの意味を確認するために定義をチェックする必要がなくなる
requiredマーカーによりコンパイラが必須パラメータのチェックを支援- 高階関数によりコレクション操作が簡潔になり、for ループの使用が 70% 削減
3. 関数宣言とアロー関数
(1) 関数宣言構文
▶ サンプル:関数宣言の方法
// 標準的な関数宣言
double calculateTax(double amount, double taxRate) {
return amount * taxRate;
}
// アロー関数(式本体)
double calculateTaxShort(double amount, double taxRate) =>
amount * taxRate;
// void 関数
void printOrderSummary(String id, double amount) {
print('Order $id: \$${amount.toStringAsFixed(2)} USD');
}
// メインエントリ
void main() {
print(calculateTax(1500.0, 0.08)); // 120.0
print(calculateTaxShort(1500.0, 0.08)); // 120.0
printOrderSummary('ORD-001', 1500.0);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 形式 | 構文 | 用途 |
|---|---|---|
| 標準関数 | { return expr; } |
複数の文 |
| アロー関数 | => expr |
単一式 return |
| void 関数 | void name() {} |
戻り値なし |
4. パラメータタイプの詳細
(1) パラメータ決定ツリー
graph TD A[関数パラメータ] --> B[位置指定<br/>デフォルトで必須] A --> C[名前付き<br/>デフォルトで任意] B --> B1[必須の位置指定] B --> B2["任意の位置指定 []"] C --> C1["必須の名前付き required"] C --> C2["デフォルト付き任意名前付き"]
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:位置指定パラメータ
// 必須の位置指定パラメータ
double calculateTotal(double amount, double taxRate) {
return amount * (1 + taxRate);
}
// 任意の位置指定パラメータ
String formatCurrency(double amount, [String currency = 'USD']) {
return '\$${amount.toStringAsFixed(2)} $currency';
}
void main() {
print(calculateTotal(1500.0, 0.08)); // 1620.0
print(formatCurrency(1500.0)); // $1500.00 USD
print(formatCurrency(1500.0, 'EUR')); // $1500.00 EUR
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:名前付きパラメータ
// 名前付きパラメータ - デフォルトで任意
double processOrder({
required String id,
required double amount,
double taxRate = 0.08,
double discountRate = 0,
String currency = 'USD',
}) {
final discounted = amount * (1 - discountRate);
final taxed = discounted * (1 + taxRate);
print('Order $id: \$${taxed.toStringAsFixed(2)} $currency');
return taxed;
}
void main() {
processOrder(id: 'ORD-001', amount: 1500.0);
processOrder(
id: 'ORD-002',
amount: 3200.0,
taxRate: 0.10,
discountRate: 0.15,
currency: 'EUR',
);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:required マーカー
// required は呼び出し側にパラメータ提供を強制する
class OrderValidator {
bool validate({
required String orderId,
required double amount,
String? customerName, // 任意 - null 許容
}) {
if (orderId.isEmpty) return false;
if (amount <= 0) return false;
return true;
}
}
void main() {
final validator = OrderValidator();
// validator.validate(orderId: 'ORD-001'); // エラー!amount が必須
validator.validate(orderId: 'ORD-001', amount: 1500.0); // OK
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| パラメータタイプ | 構文 | デフォルトで必須 | デフォルト値 |
|---|---|---|---|
| 位置指定 | Type name |
はい | なし |
| 任意の位置指定 | [Type name = default] |
いいえ | 指定可能 |
| 名前付き | {Type name} |
いいえ | null |
| 必須名前付き | {required Type name} |
はい | なし |
| デフォルト付き名前付き | {Type name = default} |
いいえ | 指定値 |
5. クロージャと変数キャプチャ
(1) クロージャの原理
クロージャは、字句スコープの変数にアクセスできる関数オブジェクトであり、そのスコープ外で呼び出された場合でもそれらにアクセスできる。
▶ サンプル:クロージャの基礎
// 関数を返す関数(クロージャ)
Function makeTaxCalculator(double taxRate) {
// taxRate は返される関数にキャプチャされる
return (double amount) => amount * taxRate;
}
void main() {
final usTax = makeTaxCalculator(0.08);
final euTax = makeTaxCalculator(0.20);
print(usTax(1500.0)); // 120.0 (8% 税)
print(euTax(1500.0)); // 300.0 (20% 税)
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:変数をキャプチャするクロージャ
void main() {
// カウンタークロージャ
int Function() makeCounter() {
int count = 0; // キャプチャされる変数
return () => ++count;
}
final counterA = makeCounter();
final counterB = makeCounter();
print(counterA()); // 1
print(counterA()); // 2
print(counterB()); // 1 (別のキャプチャ変数)
print(counterA()); // 3
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:クロージャの実践的な応用
// DataPipeline フィルタファクトリ
typedef OrderFilter = bool Function(Map<String, dynamic> order);
OrderFilter makeAmountFilter(double minAmount, {double? maxAmount}) {
return (order) {
final amount = order['amount'] as double;
if (amount < minAmount) return false;
if (maxAmount != null && amount > maxAmount) return false;
return true;
};
}
void main() {
final orders = [
{'id': 'ORD-001', 'amount': 1500.0},
{'id': 'ORD-002', 'amount': 50.0},
{'id': 'ORD-003', 'amount': 3200.0},
];
final premiumFilter = makeAmountFilter(1000);
final midRangeFilter = makeAmountFilter(100, maxAmount: 1000);
print(orders.where(premiumFilter).length); // 2
print(orders.where(midRangeFilter).length); // 1
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
6. 高階関数
(1) コレクション操作の高階関数
▶ サンプル:forEach
void main() {
final orders = ['ORD-001', 'ORD-002', 'ORD-003'];
orders.forEach((order) => print('Processing: $order'));
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:map — 変換
void main() {
final amounts = [1500.0, 3200.0, 890.0];
// 金額をフォーマット済み文字列に変換
final formatted = amounts.map((a) => '\$${a.toStringAsFixed(2)} USD').toList();
print(formatted); // [$1500.00 USD, $3200.00 USD, $890.00 USD]
// 税計算を適用
final withTax = amounts.map((a) => a * 1.08).toList();
print(withTax); // [1620.0, 3456.0, 961.2]
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:where — フィルタリング
void main() {
final orders = [
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed'},
{'id': 'ORD-002', 'amount': 50.0, 'status': 'completed'},
{'id': 'ORD-003', 'amount': 3200.0, 'status': 'pending'},
];
// 高額な完了済み注文をフィルタ
final premiumCompleted = orders
.where((o) => o['status'] == 'completed')
.where((o) => (o['amount'] as double) >= 1000)
.toList();
print('Premium completed: ${premiumCompleted.length}'); // 1
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:reduce — 集約
void main() {
final amounts = [1500.0, 3200.0, 890.0];
// 全金額を集計
final total = amounts.reduce((sum, amount) => sum + amount);
print('Total: \$${total.toStringAsFixed(2)} USD'); // $5590.00 USD
// 最大値を探す
final maxAmount = amounts.reduce((max, amount) => amount > max ? amount : max);
print('Max: \$${maxAmount.toStringAsFixed(2)} USD'); // $3200.00 USD
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
▶ サンプル:fold — 初期値付き集約
void main() {
final orders = [
{'amount': 1500.0, 'status': 'completed'},
{'amount': 3200.0, 'status': 'pending'},
{'amount': 890.0, 'status': 'completed'},
];
// 初期値とアキュムレータ付き fold
final completedRevenue = orders.fold<double>(0.0, (sum, order) {
if (order['status'] == 'completed') {
return sum + (order['amount'] as double);
}
return sum;
});
print('Completed revenue: \$${completedRevenue.toStringAsFixed(2)} USD');
// Completed revenue: $2390.00 USD
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
| 関数 | 目的 | 戻り型 | 初期値の必要性 |
|---|---|---|---|
forEach |
反復実行 | void | いいえ |
map |
各要素を変換 | Iterable<T> |
いいえ |
where |
要素をフィルタ | Iterable<T> |
いいえ |
reduce |
単一値に集約 | T | いいえ(ただしコレクションは空不可) |
fold |
初期値付き集約 | Any | はい |
7. 完全なサンプル:DataPipeline 注文分析関数ライブラリ
// ============================================
// DataPipeline 注文分析関数ライブラリ
// 関数、クロージャ、高階関数を実演
// ============================================
typedef Order = Map<String, dynamic>;
// クロージャを使ったフィルタファクトリ
bool Function(Order) makeFilter({
double? minAmount,
String? requiredStatus,
}) {
return (Order order) {
if (minAmount != null && (order['amount'] as double) < minAmount) {
return false;
}
if (requiredStatus != null && order['status'] != requiredStatus) {
return false;
}
return true;
};
}
// fold を使った集約関数
Map<String, double> aggregateByCategory(List<Order> orders) {
return orders.fold<Map<String, double>>({}, (acc, order) {
final category = (order['category'] ?? 'Uncategorized') as String;
final amount = order['amount'] as double;
acc[category] = (acc[category] ?? 0) + amount;
return acc;
});
}
// レポートエントリのフォーマット
String formatEntry(String category, double amount) =>
' $category: \$${amount.toStringAsFixed(2)} USD';
// メインパイプライン関数
void runPipeline(List<Order> orders, {double minAmount = 0}) {
final filter = makeFilter(minAmount: minAmount, requiredStatus: 'completed');
final filtered = orders.where(filter).toList();
final total = filtered.fold<double>(
0, (sum, o) => sum + (o['amount'] as double));
final byCategory = aggregateByCategory(filtered);
print('=== DataPipeline Report ===');
print('Total orders: ${orders.length}');
print('Filtered: ${filtered.length} (min: \$${minAmount} USD)');
print('Revenue: \$${total.toStringAsFixed(2)} USD');
print('\nBy Category:');
byCategory.forEach((cat, amount) => print(formatEntry(cat, amount)));
}
void main() {
final orders = <Order>[
{'id': 'ORD-001', 'amount': 1500.0, 'status': 'completed', 'category': 'Electronics'},
{'id': 'ORD-002', 'amount': 50.0, 'status': 'completed', 'category': 'Books'},
{'id': 'ORD-003', 'amount': 3200.0, 'status': 'pending', 'category': 'Electronics'},
{'id': 'ORD-004', 'amount': 890.0, 'status': 'completed', 'category': 'Clothing'},
{'id': 'ORD-005', 'amount': 2100.0, 'status': 'completed', 'category': 'Electronics'},
];
runPipeline(orders, minAmount: 100);
}
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
出力:
=== DataPipeline Report ===
Total orders: 5
Filtered: 3 (min: $100 USD)
Revenue: $4490.00 USD
By Category:
Electronics: $3600.00 USD
Clothing: $890.00 USD
❓ よくある質問
Q: 名前付きと位置指定パラメータを混在できますか? A: はい、ただし名前付きパラメータは位置指定パラメータの後に来なければなりません。例えば
void f(int a, {int? b})は有効ですが、void f({int? b}, int a)は無効です。
Q: アロー関数と通常の関数でパフォーマンスの違いはありますか? A: ありません。アロー関数はシンタックスシュガーであり、通常の関数と全く同じコードにコンパイルされます。選択はコードの可読性によります。
Q:
reduceとfoldの違いは何ですか? A:reduceは初期値を必要としませんが、コレクションは空にできず、戻り型は要素の型と一致します。foldは初期値を必要とし、空コレクションでも動作し、戻り型は異なる場合があります。一般的にfoldが推奨されます。
Q: クロージャは値渡しと参照渡しのどちらで変数をキャプチャしますか? A: Dart のクロージャは参照渡しで変数をキャプチャします(値のコピーではありません)。したがって、クロージャ内で変数を変更すると外部の変数に影響し、外部の変更はクロージャに影響します。
Q:
map/whereはListとIterableのどちらを返しますか? A:Iterableを返します(遅延評価)。Listが必要な場合は.toList()を追加してください。遅延評価とは、map/whereチェーンは走査時にのみ計算され、中間コレクションを作成しません。
Q: 関数を任意の関数に引数として渡せますか? A: はい、Dart の関数は第一級市民です。変数に代入したり、引数として渡したり、値として返したりできます。
Q:
typedefは何に使われますか? A:typedefは関数型のエイリアスを作成し、コードをより読みやすくします。例えばtypedef Validator = bool Function(String);はbool Function(String)より明確です。
📖 まとめ
- Dart の関数は第一級市民:代入、引数渡し、戻り値が可能
- 名前付きパラメータ +
requiredで関数呼び出しが自己文書化され、パラメータ混同を防ぐ - クロージャは外部変数への参照をキャプチャし、ファクトリ関数や設定可能なフィルタを実現
- 高階関数
map/where/reduce/foldはコレクション操作の中核ツール - 関数パラメータの決定:必須には
required名前付きパラメータ、任意にはデフォルト付き名前付きパラメータを使う
📝 練習問題
- 基礎(難易度 ⭐):名前付きパラメータを使って金額と通貨記号を受け取り、フォーマット済み文字列を返す
formatUSD関数を書いてください(例:formatUSD(amount: 1500.5, symbol: '€')は"€1,500.50"を返す)。 - 中級(難易度 ⭐⭐):クロージャを使って
makeDiscountCalculatorファクトリ関数を実装してください。割引率パラメータを受け取り、割引価格を計算する関数を返します。異なる割引率で 2 つの計算機を作成し、結果を比較してください。 - 挑戦(難易度 ⭐⭐⭐):一連の注文データに対して
map/where/foldのチェーンを使用してください:完了済み注文をフィルタ → カテゴリ別にグループ化 → 各カテゴリの総売上を計算 → 売上が最も高いカテゴリを見つける。forループを一切使わずにこのプロセス全体を行ってください。