Dart 関数 — 第一級市民とクロージャ

関数はコードの構成要素である — 小さな関数が組み合わさって大きなシステムを構築する。

1. 学べること


2. 開発者のリアルな物語

(1) 課題:パラメータ混同による呼び出しエラー

Bob は DataPipeline に 7 個の位置指定パラメータを持つ processOrder 関数を定義した。チームメンバーは呼び出し時にパラメータの順序をよく間違え、誰かが taxRatediscountRate として渡し、バッチ内の全 5 万件の注文で誤った割引計算が行われた。顧客の苦情が急増し、緊急対応に 2 日を費やした。

(2) 解決策:名前付きパラメータ

Dart の名前付きパラメータ {} は各パラメータに明確なラベルを与え、呼び出し時の順序依存を排除する。コンパイラは required パラメータが提供されているかもチェックできる。

DART
// 修正前: 位置指定パラメータ - 混同しやすい
// 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',
);
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

(3) 効果


3. 関数宣言とアロー関数

(1) 関数宣言構文

▶ サンプル:関数宣言の方法

DART
// 標準的な関数宣言
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);
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。
形式 構文 用途
標準関数 { return expr; } 複数の文
アロー関数 => expr 単一式 return
void 関数 void name() {} 戻り値なし

4. パラメータタイプの詳細

(1) パラメータ決定ツリー

100%
graph TD
  A[関数パラメータ] --> B[位置指定<br/>デフォルトで必須]
  A --> C[名前付き<br/>デフォルトで任意]
  B --> B1[必須の位置指定]
  B --> B2["任意の位置指定 []"]
  C --> C1["必須の名前付き required"]
  C --> C2["デフォルト付き任意名前付き"]
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:位置指定パラメータ

DART
// 必須の位置指定パラメータ
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:名前付きパラメータ

DART
// 名前付きパラメータ - デフォルトで任意
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',
  );
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:required マーカー

DART
// 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
}
TEXT
> 出力: ローカルの 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) クロージャの原理

クロージャは、字句スコープの変数にアクセスできる関数オブジェクトであり、そのスコープ外で呼び出された場合でもそれらにアクセスできる。

▶ サンプル:クロージャの基礎

DART
// 関数を返す関数(クロージャ)
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% 税)
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:変数をキャプチャするクロージャ

DART
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:クロージャの実践的な応用

DART
// 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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

6. 高階関数

(1) コレクション操作の高階関数

▶ サンプル:forEach

DART
void main() {
  final orders = ['ORD-001', 'ORD-002', 'ORD-003'];
  orders.forEach((order) => print('Processing: $order'));
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:map — 変換

DART
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]
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:where — フィルタリング

DART
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:reduce — 集約

DART
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
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

▶ サンプル:fold — 初期値付き集約

DART
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
}
TEXT
> 出力: ローカルの 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 注文分析関数ライブラリ

DART
// ============================================
// 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);
}
TEXT
> 出力: ローカルの DartPad または `dart run` で実行してください。この Dart コースの全例は Dart 3.x / Flutter 3.x ベースです。SDK バージョンにより結果が多少異なる場合があります。

出力:

TEXT
=== 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: reducefold の違いは何ですか? A: reduce は初期値を必要としませんが、コレクションは空にできず、戻り型は要素の型と一致します。fold は初期値を必要とし、空コレクションでも動作し、戻り型は異なる場合があります。一般的に fold が推奨されます。

Q: クロージャは値渡しと参照渡しのどちらで変数をキャプチャしますか? A: Dart のクロージャは参照渡しで変数をキャプチャします(値のコピーではありません)。したがって、クロージャ内で変数を変更すると外部の変数に影響し、外部の変更はクロージャに影響します。

Q: map/whereListIterable のどちらを返しますか? A: Iterable を返します(遅延評価)。List が必要な場合は .toList() を追加してください。遅延評価とは、map/where チェーンは走査時にのみ計算され、中間コレクションを作成しません。

Q: 関数を任意の関数に引数として渡せますか? A: はい、Dart の関数は第一級市民です。変数に代入したり、引数として渡したり、値として返したりできます。

Q: typedef は何に使われますか? A: typedef は関数型のエイリアスを作成し、コードをより読みやすくします。例えば typedef Validator = bool Function(String);bool Function(String) より明確です。


📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):名前付きパラメータを使って金額と通貨記号を受け取り、フォーマット済み文字列を返す formatUSD 関数を書いてください(例:formatUSD(amount: 1500.5, symbol: '€')"€1,500.50" を返す)。
  2. 中級(難易度 ⭐⭐):クロージャを使って makeDiscountCalculator ファクトリ関数を実装してください。割引率パラメータを受け取り、割引価格を計算する関数を返します。異なる割引率で 2 つの計算機を作成し、結果を比較してください。
  3. 挑戦(難易度 ⭐⭐⭐):一連の注文データに対して map/where/fold のチェーンを使用してください:完了済み注文をフィルタ → カテゴリ別にグループ化 → 各カテゴリの総売上を計算 → 売上が最も高いカテゴリを見つける。for ループを一切使わずにこのプロセス全体を行ってください。

← 前のレッスン | 次のレッスン →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%