第4章 関数とクロージャ

関数の詳細(名前付き引数、オプション引数)

位置引数(Positional Parameters)

// 通常の位置引数
String greet(String name, int age) {
  return '$nameさん、$age歳ですね';
}

void main() {
  print(greet('太郎', 25));
  // print(greet(25, '太郎'));  // エラー:順序が重要
}

オプション位置引数(Optional Positional Parameters)

// 角括弧[]で囲むとオプションになる
String createMessage(String text, [String? prefix, String suffix = '!']) {
  var message = text;
  if (prefix != null) {
    message = '$prefix$message';
  }
  return message + suffix;
}

void main() {
  print(createMessage('こんにちは'));                    // こんにちは!
  print(createMessage('こんにちは', 'お客様、'));        // お客様、こんにちは!
  print(createMessage('こんにちは', 'お客様、', '。'));  // お客様、こんにちは。
  
  // スキップはできない
  // print(createMessage('こんにちは', , '。'));  // エラー
}

名前付き引数(Named Parameters)

// 波括弧{}で囲むと名前付き引数になる
void registerUser({
  required String email,
  required String password,
  String? nickname,
  int age = 18,
  bool isAdmin = false
}) {
  print('登録情報:');
  print('  Email: $email');
  print('  Password: ${"*" * password.length}');
  print('  Nickname: ${nickname ?? "未設定"}');
  print('  Age: $age');
  print('  Admin: $isAdmin');
}

void main() {
  // 名前を指定して呼び出し(順序は自由)
  registerUser(
    email: 'test@example.com',
    password: 'secret123'
  );
  
  print('\n---\n');
  
  registerUser(
    password: 'pass456',
    email: 'user@example.com',
    nickname: '太郎',
    age: 25,
    isAdmin: true
  );
}

名前付き引数と位置引数の組み合わせ

// 位置引数の後に名前付き引数
void displayProduct(
  String name,           // 必須の位置引数
  double price,          // 必須の位置引数
  {
    String? description, // オプションの名前付き引数
    bool inStock = true, // デフォルト値付き名前付き引数
    required String category  // 必須の名前付き引数
  }
) {
  print('商品: $name ($category)');
  print('価格: ¥$price');
  if (description != null) {
    print('説明: $description');
  }
  print('在庫: ${inStock ? "あり" : "なし"}');
}

void main() {
  displayProduct(
    'ノートPC',
    120000,
    category: '電化製品',
    description: '高性能なビジネス向けノートPC',
    inStock: false
  );
}

デフォルト値として式を使用

import 'dart:math';

// デフォルト値に式を使える
String createId({String prefix = 'ID', int? number}) {
  number ??= Random().nextInt(10000);
  return '$prefix-${number.toString().padLeft(4, '0')}';
}

void main() {
  print(createId());                      // ID-1234 (ランダム)
  print(createId(prefix: 'USER'));        // USER-5678 (ランダム)
  print(createId(prefix: 'ORDER', number: 42));  // ORDER-0042
}

実践的な例

class SearchOptions {
  final String query;
  final int maxResults;
  final String sortBy;
  final bool ascending;
  
  SearchOptions({
    required this.query,
    this.maxResults = 10,
    this.sortBy = 'relevance',
    this.ascending = false
  });
  
  @override
  String toString() {
    return 'SearchOptions(query: $query, max: $maxResults, sort: $sortBy, asc: $ascending)';
  }
}

List<String> search({
  required String query,
  int limit = 10,
  String sortBy = 'relevance',
  bool ascending = false,
  List<String>? categories
}) {
  print('検索実行: $query');
  print('  制限: $limit件');
  print('  並び順: $sortBy (${ascending ? "昇順" : "降順"})');
  if (categories != null) {
    print('  カテゴリ: ${categories.join(", ")}');
  }
  
  // 実際の検索処理はここに実装
  return ['結果1', '結果2', '結果3'];
}

void main() {
  var results = search(
    query: 'Dart',
    limit: 20,
    categories: ['プログラミング', '技術']
  );
  
  print('\n結果: ${results.length}件');
}

高階関数

高階関数とは、関数を引数として受け取る、または関数を戻り値として返す関数のことです。

関数を引数として受け取る

// 高階関数の定義
void executeOperation(int a, int b, int Function(int, int) operation) {
  var result = operation(a, b);
  print('結果: $result');
}

// 様々な演算関数
int add(int x, int y) => x + y;
int multiply(int x, int y) => x * y;
int subtract(int x, int y) => x - y;

void main() {
  print('=== 関数を引数として渡す ===');
  executeOperation(10, 5, add);       // 結果: 15
  executeOperation(10, 5, multiply);  // 結果: 50
  executeOperation(10, 5, subtract);  // 結果: 5
  
  // 無名関数を直接渡す
  executeOperation(10, 5, (x, y) => x ~/ y);  // 結果: 2
}

関数を返す関数

// 関数を返す関数
Function makeMultiplier(int factor) {
  return (int value) => value * factor;
}

// より型安全なバージョン
int Function(int) createAdder(int addend) {
  return (int value) => value + addend;
}

void main() {
  print('\n=== 関数を返す ===');
  
  var double = makeMultiplier(2);
  var triple = makeMultiplier(3);
  
  print('double(5) = ${double(5)}');  // 10
  print('triple(5) = ${triple(5)}');  // 15
  
  var add10 = createAdder(10);
  var add100 = createAdder(100);
  
  print('add10(5) = ${add10(5)}');    // 15
  print('add100(5) = ${add100(5)}');  // 105
}

コレクション操作での高階関数

void main() {
  var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  
  // map - 変換
  var squared = numbers.map((n) => n * n);
  print('2乗: ${squared.toList()}');
  
  // where - フィルタリング
  var evens = numbers.where((n) => n % 2 == 0);
  print('偶数: ${evens.toList()}');
  
  // reduce - 累積
  var sum = numbers.reduce((a, b) => a + b);
  print('合計: $sum');
  
  // fold - 初期値付き累積
  var product = numbers.fold(1, (prev, element) => prev * element);
  print('積: $product');
  
  // every - すべてが条件を満たすか
  var allPositive = numbers.every((n) => n > 0);
  print('すべて正数: $allPositive');
  
  // any - いずれかが条件を満たすか
  var hasLarge = numbers.any((n) => n > 8);
  print('8より大きい数がある: $hasLarge');
}

カスタム高階関数の実装

// リトライ機能を持つ高階関数
T retry<T>(T Function() operation, {int maxAttempts = 3}) {
  for (var attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      print('試行 $attempt/$maxAttempts');
      return operation();
    } catch (e) {
      if (attempt == maxAttempts) {
        print('すべての試行が失敗しました');
        rethrow;
      }
      print('エラー: $e - 再試行します...');
    }
  }
  throw Exception('予期しないエラー');
}

// 時間計測を行う高階関数
T measureTime<T>(T Function() operation, String label) {
  var stopwatch = Stopwatch()..start();
  var result = operation();
  stopwatch.stop();
  print('$label: ${stopwatch.elapsedMilliseconds}ms');
  return result;
}

// 条件付き実行
void runIf(bool condition, void Function() action) {
  if (condition) {
    action();
  }
}

void main() {
  print('=== カスタム高階関数 ===\n');
  
  // リトライの例
  var result = retry(() {
    // ランダムに成功/失敗
    if (DateTime.now().millisecond % 3 == 0) {
      return 'Success!';
    }
    throw Exception('Failed');
  });
  print('結果: $result\n');
  
  // 時間計測の例
  measureTime(() {
    var sum = 0;
    for (var i = 0; i < 1000000; i++) {
      sum += i;
    }
    return sum;
  }, '100万回の加算');
  
  // 条件付き実行
  var isDebug = true;
  runIf(isDebug, () {
    print('デバッグモードで実行中');
  });
}

関数の合成

// 関数を合成する高階関数
Function compose(Function f, Function g) {
  return (x) => f(g(x));
}

// パイプライン処理
T pipe<T>(T value, List<T Function(T)> functions) {
  var result = value;
  for (var func in functions) {
    result = func(result);
  }
  return result;
}

void main() {
  print('\n=== 関数の合成 ===');
  
  // 個別の関数
  int addTwo(int x) => x + 2;
  int multiplyByThree(int x) => x * 3;
  int square(int x) => x * x;
  
  // 関数を合成
  var addThenMultiply = compose(multiplyByThree, addTwo);
  print('(5 + 2) × 3 = ${addThenMultiply(5)}');  // 21
  
  // パイプライン
  var result = pipe(5, [
    addTwo,           // 5 + 2 = 7
    multiplyByThree,  // 7 × 3 = 21
    square            // 21² = 441
  ]);
  print('パイプライン結果: $result');
}

無名関数とラムダ式

無名関数の基本

void main() {
  // 通常の関数定義
  int add1(int a, int b) {
    return a + b;
  }
  
  // 無名関数を変数に代入
  var add2 = (int a, int b) {
    return a + b;
  };
  
  // ラムダ式(アロー関数)
  var add3 = (int a, int b) => a + b;
  
  print('add1(3, 5) = ${add1(3, 5)}');
  print('add2(3, 5) = ${add2(3, 5)}');
  print('add3(3, 5) = ${add3(3, 5)}');
}

様々な形式の無名関数

void main() {
  // 引数なし
  var greet = () => print('こんにちは!');
  greet();
  
  // 1つの引数
  var double = (int n) => n * 2;
  print('double(5) = ${double(5)}');
  
  // 複数の引数
  var divide = (int a, int b) => a / b;
  print('10 / 3 = ${divide(10, 3)}');
  
  // 複数行のボディ
  var calculate = (int a, int b) {
    var sum = a + b;
    var product = a * b;
    print('和: $sum, 積: $product');
    return sum + product;
  };
  var result = calculate(5, 3);
  print('結果: $result');
  
  // 型推論を利用
  var multiply = (a, b) => a * b;  // 型が推論される
  print('5 × 3 = ${multiply(5, 3)}');
}

無名関数の実践的な使用例

void main() {
  var numbers = [1, 2, 3, 4, 5];
  
  // リスト操作で無名関数を使用
  print('=== リスト操作 ===');
  
  // map
  var doubled = numbers.map((n) => n * 2).toList();
  print('2倍: $doubled');
  
  // where
  var evens = numbers.where((n) => n % 2 == 0).toList();
  print('偶数: $evens');
  
  // forEach
  numbers.forEach((n) => print('値: $n'));
  
  // sort(複雑な比較)
  var words = ['banana', 'apple', 'cherry', 'date'];
  words.sort((a, b) => a.length.compareTo(b.length));
  print('長さ順: $words');
  
  // reduce
  var sum = numbers.reduce((a, b) => a + b);
  print('合計: $sum');
}

イベントハンドラとしての無名関数

class Button {
  String label;
  void Function()? onPressed;
  
  Button(this.label);
  
  void press() {
    print('[$label]ボタンが押されました');
    onPressed?.call();
  }
}

void main() {
  print('\n=== イベントハンドラ ===');
  
  var saveButton = Button('保存');
  saveButton.onPressed = () {
    print('データを保存しています...');
    print('保存完了!');
  };
  
  var cancelButton = Button('キャンセル');
  cancelButton.onPressed = () => print('操作がキャンセルされました');
  
  saveButton.press();
  print('');
  cancelButton.press();
}

コールバックパターン

// 非同期処理のシミュレーション
void fetchData(
  void Function(String data) onSuccess,
  void Function(String error) onError
) {
  print('データを取得中...');
  
  // 処理のシミュレーション
  var success = DateTime.now().second % 2 == 0;
  
  Future.delayed(Duration(seconds: 1), () {
    if (success) {
      onSuccess('{"name": "太郎", "age": 25}');
    } else {
      onError('ネットワークエラー');
    }
  });
}

void main() {
  print('\n=== コールバックパターン ===');
  
  fetchData(
    (data) {
      print('成功: $data');
    },
    (error) {
      print('エラー: $error');
    }
  );
  
  // メインスレッドは続行
  print('処理を継続中...');
}

クロージャとスコープ

クロージャとは、外側のスコープの変数を「キャプチャ」して保持できる関数のことです。

基本的なクロージャ

Function makeCounter() {
  var count = 0;  // この変数はクロージャによってキャプチャされる
  
  return () {
    count++;
    return count;
  };
}

void main() {
  print('=== 基本的なクロージャ ===');
  
  var counter1 = makeCounter();
  var counter2 = makeCounter();
  
  print('counter1: ${counter1()}');  // 1
  print('counter1: ${counter1()}');  // 2
  print('counter1: ${counter1()}');  // 3
  
  print('counter2: ${counter2()}');  // 1
  print('counter2: ${counter2()}');  // 2
  
  // 各カウンターは独立した状態を保持
}

より実用的なクロージャの例

class BankAccount {
  String _accountNumber;
  double _balance;
  
  BankAccount(this._accountNumber, this._balance);
  
  // クロージャを返すメソッド
  Function get deposit {
    return (double amount) {
      if (amount > 0) {
        _balance += amount;
        print('¥$amount を入金しました。残高: ¥$_balance');
      }
    };
  }
  
  Function get withdraw {
    return (double amount) {
      if (amount > 0 && amount <= _balance) {
        _balance -= amount;
        print('¥$amount を出金しました。残高: ¥$_balance');
      } else {
        print('出金できません');
      }
    };
  }
  
  double get balance => _balance;
}

void main() {
  print('\n=== 銀行口座の例 ===');
  
  var account = BankAccount('12345', 10000);
  
  var depositMoney = account.deposit;
  var withdrawMoney = account.withdraw;
  
  depositMoney(5000);
  withdrawMoney(3000);
  withdrawMoney(20000);  // 残高不足
  
  print('最終残高: ¥${account.balance}');
}

クロージャによる情報の隠蔽

Map<String, Function> createUser(String name, String email) {
  // プライベートな変数(外部から直接アクセスできない)
  var _password = '';
  var _loginCount = 0;
  
  return {
    'getName': () => name,
    'getEmail': () => email,
    'setPassword': (String newPassword) {
      if (newPassword.length >= 8) {
        _password = newPassword;
        print('パスワードを設定しました');
      } else {
        print('パスワードは8文字以上必要です');
      }
    },
    'login': (String password) {
      if (_password.isEmpty) {
        print('パスワードが設定されていません');
        return false;
      }
      if (_password == password) {
        _loginCount++;
        print('ログイン成功 (${_loginCount}回目)');
        return true;
      } else {
        print('パスワードが違います');
        return false;
      }
    },
    'getLoginCount': () => _loginCount
  };
}

void main() {
  print('\n=== 情報の隠蔽 ===');
  
  var user = createUser('太郎', 'taro@example.com');
  
  print('名前: ${user['getName']!()}');
  print('Email: ${user['getEmail']!()}');
  
  user['setPassword']!('short');      // 短すぎる
  user['setPassword']!('password123'); // OK
  
  user['login']!('wrongpass');        // 失敗
  user['login']!('password123');      // 成功
  user['login']!('password123');      // 成功
  
  print('ログイン回数: ${user['getLoginCount']!()}');
}

スコープチェーン

var globalVar = 'グローバル';

void demonstrateScope() {
  var outerVar = '外側';
  
  void innerFunction() {
    var innerVar = '内側';
    
    print('innerVar: $innerVar');
    print('outerVar: $outerVar');    // 外側のスコープにアクセス
    print('globalVar: $globalVar');   // グローバルスコープにアクセス
  }
  
  innerFunction();
  // print(innerVar);  // エラー:innerVarはinnerFunctionのスコープ内のみ
}

void main() {
  print('\n=== スコープチェーン ===');
  demonstrateScope();
}

レキシカルスコープ

var message = '外側のメッセージ';

Function createFunction() {
  var message = '関数内のメッセージ';
  
  return () {
    print(message);  // 定義時のスコープのmessageを参照
  };
}

void main() {
  print('\n=== レキシカルスコープ ===');
  
  var func = createFunction();
  func();  // '関数内のメッセージ'が出力される
  
  // funcは定義された場所のmessageを記憶している
}

クロージャを使った実装パターン

// メモ化(キャッシング)
Function memoize(int Function(int) func) {
  var cache = <int, int>{};
  
  return (int n) {
    if (cache.containsKey(n)) {
      print('キャッシュから取得: $n');
      return cache[n]!;
    }
    print('計算中: $n');
    var result = func(n);
    cache[n] = result;
    return result;
  };
}

// フィボナッチ数(遅い実装)
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// デバウンス関数
Function debounce(Function func, Duration delay) {
  Timer? timer;
  
  return ([args]) {
    timer?.cancel();
    timer = Timer(delay, () {
      Function.apply(func, args ?? []);
    });
  };
}

void main() async {
  print('\n=== メモ化 ===');
  var memoizedFib = memoize(fibonacci);
  
  print('fib(10) = ${memoizedFib(10)}');
  print('fib(10) = ${memoizedFib(10)}');  // キャッシュから
  print('fib(5) = ${memoizedFib(5)}');    // キャッシュから
  
  print('\n=== デバウンス ===');
  var searchFunction = debounce(() {
    print('検索を実行: ${DateTime.now()}');
  }, Duration(milliseconds: 500));
  
  // 連続して呼び出す
  searchFunction();
  searchFunction();
  searchFunction();
  // 最後の呼び出しから500ms後に1回だけ実行される
  
  await Future.delayed(Duration(seconds: 1));
}

import 'dart:async';

カスケード記法

カスケード記法(..)を使うと、同じオブジェクトに対して複数の操作を連続して行えます。

カスケード記法の基本

class Person {
  String? name;
  int? age;
  String? address;
  
  void introduce() {
    print('こんにちは、$nameです。$age歳、$address在住です。');
  }
  
  void celebrateBirthday() {
    age = (age ?? 0) + 1;
    print('誕生日おめでとう!$age歳になりました。');
  }
}

void main() {
  print('=== 通常の方法 ===');
  var person1 = Person();
  person1.name = '太郎';
  person1.age = 25;
  person1.address = '東京';
  person1.introduce();
  
  print('\n=== カスケード記法 ===');
  var person2 = Person()
    ..name = '花子'
    ..age = 23
    ..address = '大阪'
    ..introduce();
}

リストでのカスケード記法

void main() {
  print('\n=== リストでの使用 ===');
  
  // 通常の方法
  var list1 = <int>[];
  list1.add(1);
  list1.add(2);
  list1.add(3);
  list1.sort();
  print('list1: $list1');
  
  // カスケード記法
  var list2 = <int>[]
    ..add(3)
    ..add(1)
    ..add(2)
    ..sort();
  print('list2: $list2');
  
  // より複雑な例
  var numbers = <int>[]
    ..addAll([5, 2, 8, 1, 9])
    ..sort()
    ..removeAt(0)
    ..add(10);
  print('numbers: $numbers');
}

null許容カスケード(?..)

class Config {
  String? theme;
  int? fontSize;
  bool? darkMode;
  
  void apply() {
    print('設定を適用: theme=$theme, fontSize=$fontSize, darkMode=$darkMode');
  }
}

void main() {
  print('\n=== Null許容カスケード ===');
  
  Config? config1 = Config();
  config1
    ..theme = 'modern'
    ..fontSize = 14
    ..darkMode = true
    ..apply();
  
  Config? config2 = null;
  // config2がnullの場合、以下の操作はすべてスキップされる
  config2
    ?..theme = 'classic'
    ..fontSize = 16
    ..apply();
  
  print('config2はnullなので何も起きません');
}

カスケード記法とメソッドチェーンの組み合わせ

class StringBuilder {
  final StringBuffer _buffer = StringBuffer();
  
  StringBuilder append(String text) {
    _buffer.write(text);
    return this;  // メソッドチェーン用
  }
  
  StringBuilder appendLine(String text) {
    _buffer.writeln(text);
    return this;
  }
  
  void clear() {
    _buffer.clear();
  }
  
  @override
  String toString() => _buffer.toString();
}

void main() {
  print('\n=== メソッドチェーンとの組み合わせ ===');
  
  // メソッドチェーン
  var result1 = StringBuilder()
    .append('Hello')
    .append(' ')
    .append('World')
    .toString();
  print('メソッドチェーン: $result1');
  
  // カスケード記法
  var builder = StringBuilder()
    ..append('Dart')
    ..append(' is ')
    ..append('awesome!');
  print('カスケード: $builder');
}

実践的な使用例

class Email {
  String? from;
  String? to;
  String? subject;
  String? body;
  List<String> attachments = [];
  bool isHtml = false;
  
  void send() {
    print('メール送信:');
    print('  From: $from');
    print('  To: $to');
    print('  Subject: $subject');
    print('  Body: $body');
    print('  Attachments: ${attachments.join(", ")}');
    print('  HTML: $isHtml');
  }
}

class QueryBuilder {
  String? _table;
  List<String> _columns = [];
  String? _where;
  String? _orderBy;
  int? _limit;
  
  QueryBuilder select(List<String> columns) {
    _columns = columns;
    return this;
  }
  
  QueryBuilder from(String table) {
    _table = table;
    return this;
  }
  
  QueryBuilder where(String condition) {
    _where = condition;
    return this;
  }
  
  QueryBuilder orderBy(String column) {
    _orderBy = column;
    return this;
  }
  
  QueryBuilder limit(int count) {
    _limit = count;
    return this;
  }
  
  String build() {
    var query = 'SELECT ${_columns.join(", ")} FROM $_table';
    if (_where != null) query += ' WHERE $_where';
    if (_orderBy != null) query += ' ORDER BY $_orderBy';
    if (_limit != null) query += ' LIMIT $_limit';
    return query;
  }
}

void main() {
  print('\n=== メール送信の例 ===');
  Email()
    ..from = 'sender@example.com'
    ..to = 'receiver@example.com'
    ..subject = '重要なお知らせ'
    ..body = 'これは重要なメッセージです。'
    ..attachments.addAll(['document.pdf', 'image.jpg'])
    ..isHtml = false
    ..send();
  
  print('\n=== SQLクエリビ
  print('\n=== SQLクエリビルダーの例 ===');
  
  // メソッドチェーン方式
  var query1 = QueryBuilder()
    .select(['id', 'name', 'email'])
    .from('users')
    .where('age > 18')
    .orderBy('name')
    .limit(10)
    .build();
  print('Query 1:\n$query1\n');
  
  // カスケード記法方式
  var builder = QueryBuilder()
    ..select(['product_name', 'price', 'stock'])
    ..from('products')
    ..where('stock > 0')
    ..orderBy('price DESC')
    ..limit(5);
  var query2 = builder.build();
  print('Query 2:\n$query2');
}

UIコンポーネントでのカスケード記法

class Button {
  String? text;
  String? color;
  double? width;
  double? height;
  void Function()? onPressed;
  
  void render() {
    print('Button(text: $text, color: $color, size: ${width}x$height)');
  }
}

class Dialog {
  String? title;
  String? message;
  List<Button> buttons = [];
  bool? dismissible;
  
  void show() {
    print('=== ダイアログ表示 ===');
    print('タイトル: $title');
    print('メッセージ: $message');
    print('閉じられる: ${dismissible ?? true}');
    print('ボタン:');
    for (var button in buttons) {
      button.render();
    }
  }
}

void main() {
  print('\n=== UIコンポーネント ===');
  
  Dialog()
    ..title = '確認'
    ..message = 'この操作を実行してもよろしいですか?'
    ..dismissible = false
    ..buttons.addAll([
      Button()
        ..text = 'キャンセル'
        ..color = 'gray'
        ..width = 100
        ..height = 40
        ..onPressed = () => print('キャンセルされました'),
      Button()
        ..text = 'OK'
        ..color = 'blue'
        ..width = 100
        ..height = 40
        ..onPressed = () => print('実行されました')
    ])
    ..show();
}

カスケード記法のネスト

class Address {
  String? street;
  String? city;
  String? country;
  String? postalCode;
  
  @override
  String toString() => '$street, $city, $country $postalCode';
}

class Company {
  String? name;
  Address? address;
  List<String> departments = [];
  
  void display() {
    print('会社名: $name');
    print('住所: $address');
    print('部署: ${departments.join(", ")}');
  }
}

void main() {
  print('\n=== ネストしたカスケード ===');
  
  Company()
    ..name = 'テクノロジー株式会社'
    ..address = (Address()
      ..street = '1-2-3 技術通り'
      ..city = '東京'
      ..country = '日本'
      ..postalCode = '100-0001')
    ..departments.addAll(['開発部', '営業部', '人事部'])
    ..display();
}

Mapとカスケード記法

void main() {
  print('\n=== Mapでの使用 ===');
  
  var config = <String, dynamic>{}
    ..['appName'] = 'MyApp'
    ..['version'] = '1.0.0'
    ..['debug'] = true
    ..['maxConnections'] = 100;
  
  print('設定: $config');
  
  // より複雑な例
  var settings = <String, dynamic>{}
    ..['ui'] = {}
    ..['ui']['theme'] = 'dark'
    ..['ui']['fontSize'] = 14
    ..['network'] = {}
    ..['network']['timeout'] = 30
    ..['network']['retry'] = 3;
  
  print('詳細設定: $settings');
}

カスケード記法のベストプラクティス

class FormValidator {
  final Map<String, String> _errors = {};
  String? _currentField;
  
  FormValidator field(String name) {
    _currentField = name;
    return this;
  }
  
  FormValidator required(String? value) {
    if (value == null || value.isEmpty) {
      _errors[_currentField!] = '${_currentField}は必須です';
    }
    return this;
  }
  
  FormValidator email(String? value) {
    if (value != null && !value.contains('@')) {
      _errors[_currentField!] = '有効なメールアドレスを入力してください';
    }
    return this;
  }
  
  FormValidator minLength(String? value, int length) {
    if (value != null && value.length < length) {
      _errors[_currentField!] = '$length文字以上入力してください';
    }
    return this;
  }
  
  bool get isValid => _errors.isEmpty;
  Map<String, String> get errors => _errors;
}

void main() {
  print('\n=== フォームバリデーション ===');
  
  var validator = FormValidator()
    ..field('email')
    ..required('user@example.com')
    ..email('user@example.com')
    ..field('password')
    ..required('pass')
    ..minLength('pass', 8)
    ..field('username')
    ..required('');
  
  if (validator.isValid) {
    print('バリデーション成功');
  } else {
    print('バリデーションエラー:');
    validator.errors.forEach((field, error) {
      print('  $field: $error');
    });
  }
}

カスケード記法 vs メソッドチェーン

class Calculator {
  double _value = 0;
  
  // メソッドチェーン用(thisを返す)
  Calculator add(double n) {
    _value += n;
    return this;
  }
  
  Calculator subtract(double n) {
    _value -= n;
    return this;
  }
  
  Calculator multiply(double n) {
    _value *= n;
    return this;
  }
  
  // カスケード記法用(voidでもOK)
  void reset() {
    _value = 0;
  }
  
  void display() {
    print('現在の値: $_value');
  }
  
  double get result => _value;
}

void main() {
  print('\n=== メソッドチェーン vs カスケード ===');
  
  // メソッドチェーン
  var calc1 = Calculator()
    .add(10)
    .multiply(2)
    .subtract(5);
  print('メソッドチェーン結果: ${calc1.result}');
  
  // カスケード記法
  var calc2 = Calculator()
    ..add(10)
    ..multiply(2)
    ..subtract(5)
    ..display();  // voidメソッドも呼べる
  
  // 違いの例
  print('\n=== 戻り値の違い ===');
  
  // メソッドチェーンは最後のメソッドの戻り値を返す
  var chainResult = Calculator().add(5).add(3).result;
  print('チェーン結果: $chainResult');
  
  // カスケードは最初のオブジェクトを返す
  var cascadeObject = (Calculator()
    ..add(5)
    ..add(3));
  print('カスケード結果: ${cascadeObject.result}');
}

実践的な総合例

class HttpRequest {
  String? _url;
  String _method = 'GET';
  Map<String, String> _headers = {};
  Map<String, dynamic>? _body;
  Duration? _timeout;
  
  HttpRequest url(String url) {
    _url = url;
    return this;
  }
  
  HttpRequest method(String method) {
    _method = method;
    return this;
  }
  
  HttpRequest header(String key, String value) {
    _headers[key] = value;
    return this;
  }
  
  HttpRequest headers(Map<String, String> headers) {
    _headers.addAll(headers);
    return this;
  }
  
  HttpRequest body(Map<String, dynamic> body) {
    _body = body;
    return this;
  }
  
  HttpRequest timeout(Duration duration) {
    _timeout = duration;
    return this;
  }
  
  void send() {
    print('HTTP Request:');
    print('  Method: $_method');
    print('  URL: $_url');
    print('  Headers: $_headers');
    if (_body != null) print('  Body: $_body');
    if (_timeout != null) print('  Timeout: $_timeout');
  }
}

class Logger {
  String _level = 'INFO';
  String? _tag;
  bool _timestamp = true;
  
  void setLevel(String level) => _level = level;
  void setTag(String tag) => _tag = tag;
  void enableTimestamp(bool enabled) => _timestamp = enabled;
  
  void log(String message) {
    var prefix = '';
    if (_timestamp) {
      prefix += '[${DateTime.now()}] ';
    }
    if (_tag != null) {
      prefix += '[$_tag] ';
    }
    prefix += '[$_level] ';
    print('$prefix$message');
  }
}

void main() {
  print('\n=== HTTP リクエスト ===');
  
  // メソッドチェーンとカスケードの混在
  HttpRequest()
    .url('https://api.example.com/users')
    .method('POST')
    ..headers({
      'Content-Type': 'application/json',
      'Authorization': 'Bearer token123'
    })
    ..body({
      'name': '太郎',
      'email': 'taro@example.com'
    })
    ..timeout(Duration(seconds: 30))
    ..send();
  
  print('\n=== ロガー ===');
  
  var logger = Logger()
    ..setLevel('DEBUG')
    ..setTag('MyApp')
    ..enableTimestamp(true);
  
  logger
    ..log('アプリケーションを起動しました')
    ..log('データベースに接続しました')
    ..setLevel('ERROR')
    ..log('エラーが発生しました');
}

カスケード記法を使う際の注意点

void main() {
  print('\n=== 注意点 ===');
  
  // 1. カスケードは元のオブジェクトを返す
  var list = [1, 2, 3];
  var result1 = list..add(4);  // listを返す
  print('result1 is list: ${identical(result1, list)}');  // true
  
  // 2. メソッドチェーンとの違い
  var numbers = [3, 1, 2];
  // var sorted = numbers..sort();  // これはnumbersを返す(破壊的)
  var sorted = [...numbers]..sort();  // コピーしてからソート
  print('元のリスト: $numbers');
  print('ソート済み: $sorted');
  
  // 3. 式の中でカスケードを使う場合は括弧が必要
  var length = ([1, 2, 3]..add(4)).length;
  print('長さ: $length');
  
  // 4. nullセーフティに注意
  List<int>? nullableList;
  // nullableList..add(1);  // エラー
  nullableList?..add(1);    // OK(nullの場合は何もしない)
  
  // 5. voidメソッドでも使える(メソッドチェーンとの大きな違い)
  var builder = StringBuffer()
    ..write('Hello')
    ..write(' ')
    ..write('World')
    ..clear()  // voidメソッド
    ..write('Dart');
  print('結果: $builder');
}

まとめ

この章では、Dartの関数とクロージャについて詳しく学びました:

学んだこと

  1. 関数の詳細
    • 位置引数とオプション位置引数
    • 名前付き引数とrequiredキーワード
    • デフォルト値の設定
  2. 高階関数
    • 関数を引数として受け取る
    • 関数を戻り値として返す
    • 関数の合成とパイプライン
  3. 無名関数とラムダ式
    • 無名関数の定義と使用
    • アロー構文による簡潔な記述
    • コールバックパターン
  4. クロージャとスコープ
    • 外側のスコープの変数のキャプチャ
    • レキシカルスコープの理解
    • 状態の保持とカプセル化
  5. カスケード記法
    • 連続した操作の簡潔な記述
    • メソッドチェーンとの違い
    • Null許容カスケード(?..)

ポイント

  • 関数は第一級オブジェクト:変数に代入したり、引数として渡したりできる
  • クロージャは強力:状態を保持したり、情報を隠蔽したりできる
  • カスケード記法は便利:オブジェクトの初期化や設定に最適
  • 適材適所:メソッドチェーンとカスケード記法を状況に応じて使い分ける

次の章では、Dartのクラスとオブジェクト指向プログラミングについて深く学んでいきます!

No responses yet

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です