Swift & Objective-C

デリゲート(Delegate)vs クロージャ(Closure)、コールバックの選び方3つ

デリゲートとクロージャは同じコールバック要件に対応しますが、性質は異なります。同じ例を並べて実装し、実用上の5つの違いと、イベント数・関係の寿命・戻り値の要否で選ぶ基準を整理します。

読了 8 分
デリゲート(Delegate)vs クロージャ(Closure)、コールバックの選び方3つのカバー画像

「このコールバックはデリゲートにしますか、それともクロージャで受けますか?」iOSのコードレビューで特によく出る質問です。

どちらも「何か起きたら知らせて」という同じ要件に対応する手段なので、機能だけを見ればどちらでも実装できます。

UIKitはデリゲートだらけです(UITableViewDelegate、UITextFieldDelegate)。一方、Appleの新しいAPIはクロージャを受け取り、チームごとに規約も異なります。

この記事では、同じ例を2つの方式で並べて実装し、違いを明らかにしたうえで選択基準を整理します。

デリゲートパターンの構文やオブザーバーとの比較は別の記事で、クロージャのキャプチャや循環参照の仕組みはクロージャ編で解説しました。ここでは両記事の接点である「選択」に集中します。

同じ問題に2つの答え — 並べて比較する

画像ピッカー画面を作るとしましょう。ユーザーが写真を選んだら、呼び出し元の画面に知らせる必要があります。

デリゲート版です。

protocol ImagePickerDelegate: AnyObject {
    func imagePicker(_ picker: ImagePickerVC, didSelect image: UIImage)
    func imagePickerDidCancel(_ picker: ImagePickerVC)
}

final class ImagePickerVC: UIViewController {
    weak var delegate: ImagePickerDelegate?

    private func selectionDone(_ image: UIImage) {
        delegate?.imagePicker(self, didSelect: image)
    }
}

// 呼び出し側
extension ProfileVC: ImagePickerDelegate {
    func imagePicker(_ picker: ImagePickerVC, didSelect image: UIImage) {
        avatarView.image = image
    }
    func imagePickerDidCancel(_ picker: ImagePickerVC) { /* 無視 */ }
}

クロージャ版です。

final class ImagePickerVC: UIViewController {
    var onSelect: ((UIImage) -> Void)?
    var onCancel: (() -> Void)?

    private func selectionDone(_ image: UIImage) {
        onSelect?(image)
    }
}

// 呼び出し側
let picker = ImagePickerVC()
picker.onSelect = { [weak self] image in
    self?.avatarView.image = image
}

動作は同じです。違いは構造にあります。

デリゲートは通信契約をプロトコルという型として先に宣言し、受け手が契約全体に準拠します。クロージャは契約を設けず、イベントごとに値として受け渡します。

この構造上の違いが、次の実用的な差につながります。

5つの実用的な違い

1つ目は、イベントが増えたときの拡張性です。 デリゲートならイベントが5個、10個に増えても、プロトコルにメソッドを追加できます。

関連するイベントが1つの契約にまとめられ、準拠側は「この画面との対話全体」を1つのextensionに集約して実装できます。UITableViewDelegateに数十個のメソッドがあっても管理できる理由です。

クロージャではイベントごとにプロパティが1つずつ増えます。3、4個を超えると設定コードが分散し、「どのコールバックを接続し忘れたか」をコンパイラが検出できません。

デリゲートでは必須メソッドの未実装がコンパイルエラーになりますが、クロージャプロパティはnilのまま静かに通過します。

2つ目は、設定箇所との距離です。 クロージャの強みは、イベントを消費するコードをイベント発生元の呼び出し直後に置けることです。

ピッカーを開くコードと結果を受け取るコードが3行以内にあるため、流れを一目で読めます。デリゲートでは設定(delegate = self)と実装(extension)がファイル内で離れています。

一度きりのインタラクションには、その作法は大げさです。ネットワーク要求の完了、アラートボタンへの応答、アニメーション終了のような「一度起きて終わる」イベントでクロージャが標準になった理由です。

3つ目は、状態とアイデンティティです。 デリゲートメソッドでは慣例として送信元が第1引数に入ります(imagePicker(_:didSelect:)のpicker)。

1つの画面でテーブルビューを2つ使う場合、どちらから来たイベントかを区別できるのは、この慣例のおかげです。

クロージャでは送信元が自動的に渡されないため、同じ状況ならクロージャを2つ接続するか、送信元を引数として明示的に設計する必要があります。

4つ目は、メモリ管理の落とし穴がある場所です。 どちらにも循環参照の危険がありますが、落とし穴の形は異なります。

デリゲートは宣言時にweak var delegate一度指定すれば終わりで、慣例も強いためミスは多くありません。クロージャは接続する場所ごとに[weak self]を判断する必要があります。

ARC(Automatic Reference Counting、自動参照カウント)の記事で整理したとおりです。所有関係のない一回限りの実行ならweakは不要ですが、プロパティに保存されるコールバックは循環参照の候補になります。

その判断を利用箇所ごとに繰り返すわけです。ミスが入り込む余地はクロージャのほうが広くなります。

5つ目は、テストと再利用です。 クロージャはテストで軽量です。

モックオブジェクトを用意せず、テスト本体でコールバックを直接接続して呼び出しの有無を検証できるからです。デリゲートではテスト用のスパイククラスが必要になり、準備コードが増えます。

その代わり、デリゲートプロトコルは「このコンポーネントとの通信方法」を文書化する役割を果たします。複数の画面で同じコンポーネントを再利用する場合、契約が明示的であることが利点です。

会議室の契約書と窓口のメモにたとえた、デリゲートとクロージャの比較イラスト
デリゲートは会議室の契約書、クロージャは窓口のメモ

選択基準 — イベント数・寿命・方向で決める

違いが分かったところで、基準にまとめます。3つの質問で、ほとんどのケースを整理できます。

質問1 — イベントはいくつあるか。 1つか2つならクロージャ、3つを超えて今後も増える関係ならデリゲートです。

イベントのまとまりが「対話」に近づくほど、契約(プロトコル)の価値は高まります。

質問2 — 関係の寿命はどの程度か。 要求と応答のように短く終わるならクロージャ、画面が生きている間ずっと継続的にやり取りする関係(スクロールイベント、テキスト編集中の検証)ならデリゲートです。

長く続く関係ほど、1か所のweak delegateの安全性が、利用箇所ごとに[weak self]を判断するより有利になります。

質問3 — 値を返してもらう必要があるか。 デリゲートメソッドは戻り値を持てます。

textField(_:shouldChangeCharactersIn:)が代表例です。「実行してよいか」を尋ねる問い合わせ型の通信は、デリゲートの得意分野です。

クロージャでも戻り値を設計できますが、保存されたOptionalクロージャの戻り値をどう扱うか(nilならデフォルト値は?)が不自然になりがちです。

境界が曖昧な場面ももちろんあります。そんなとき参考になる外部シグナルが、Appleの最近の方向性です。

UIActionベースのボタンハンドラーや、UICollectionViewDiffableDataSourceのクロージャプロバイダーがその例です。async/awaitへの移行(エラー処理編で見たcompletionハンドラーの世代交代)も同じ流れです。

一度きりの通信やデータ提供型の通信は、着実にクロージャ・async側へ移行しています。

一方、継続的なインタラクションの標準はデリゲートとして残っています。UITableViewDelegateやUINavigationControllerDelegateがその例です。

フレームワークにおけるこの分業構造は、上の3つの質問と正確に一致します。

アンチパターンを1つだけ挙げておきます。メソッドが1つしかないデリゲートプロトコルを、画面ごとに新しく作るコードです。

単一イベントのために、プロトコル宣言、準拠、weakプロパティ、extensionという4段階の作法を踏むのは、ほとんどの場合過剰設計です。そこはクロージャ1行で十分です。

逆に、クロージャプロパティが6、7個も付いたクラスは、デリゲートにまとめるべきサインです。

イベント数・関係の寿命・戻り値という3つの質問で分岐するコールバック選択フロー
イベント数・関係の寿命・戻り値、3つの質問で決まります

まとめ

  • デリゲートとクロージャは、同じ要件(「何か起きたら知らせて」)に対する2つの実装です。違いの根本は、契約を型(プロトコル)として宣言するか、イベントを値として受け渡すかにあります。
  • 実用上の5つの違いは、イベントの拡張性と未実装の検出はデリゲート、設定箇所の凝集はクロージャ、送信元の識別はデリゲートの慣例、メモリ上の落とし穴の広さはクロージャ、テストの軽さはクロージャが優位です。
  • 選択は3つの質問で決まります。イベントは3つ以上か、関係は長く続くか、戻り値は必要か。すべて「いいえ」ならクロージャ、1つでも強く「はい」ならデリゲートです。
  • 単一メソッドのデリゲートプロトコルと、5、6個のクロージャプロパティは、互いに逆方向の過剰設計サインです。

2つのツールをさらに詳しく知りたい方は、あわせて読みたい記事をご覧ください。デリゲートパターン編では、オブザーバーとの比較と1対1通信の定石を扱っています。

クロージャ完全解説編では、キャプチャ・weak self・escapingを整理しています。

あわせて読みたい