Swift 與 Objective-C

Delegate(代理)vs Closure(閉包):選擇回呼的 3 個判準

Delegate 與 Closure 都能處理相同的回呼需求,但特性不同。本文以相同範例並列實作,整理出五項實際差異,以及依事件數量、關係存續時間與是否需要回傳值來選擇的判準。

閱讀 6 分鐘
Delegate(代理)vs Closure(閉包):選擇回呼的 3 個判準 封面圖

「這個回呼要抽成 Delegate,還是用 Closure 接收?」這是 iOS 程式碼審查中特別常出現的問題。

兩者都是處理「發生事情時通知我」這項需求的工具,因此只看功能的話,哪一種都能實作。

UIKit 裡充滿 Delegate(UITableViewDelegate、UITextFieldDelegate)。但 Apple 的新 API 會接收 Closure,而各團隊的慣例也不盡相同。

本文會用相同範例並列實作兩種方式,呈現差異並整理選擇判準。

Delegate 模式本身的語法與和 Observer 的比較已在另一篇文章中說明,Closure 的捕捉與循環參考原理也在 Closure 篇整理過。本文聚焦於兩篇文章的交集:如何選擇。

相同問題,兩種答案 — 並列比較

假設我們要製作圖片選擇器畫面。使用者選取照片後,必須通知呼叫它的畫面。

這是 Delegate 版本。

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) { /* 忽略 */ }
}

這是 Closure 版本。

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
}

行為相同,差異在結構。

Delegate 會先將通訊契約宣告為協定這個型別,再由接收端採用整份契約。Closure 則不需要契約,而是把每個事件當成值來傳遞。

這項結構差異會延伸成以下實際差異。

五項實際差異

第一,事件增加時的擴充性。 Delegate 即使從 5 個事件增加到 10 個,只要在協定中加入方法即可。

相關事件會被綁在同一份契約中,採用者也會在同一個 extension 集中實作「與這個畫面往來的完整對話」。這就是 UITableViewDelegate 即使有數十個方法仍能維護的原因。

Closure 則會為每個事件增加一個屬性。超過三、四個後,設定程式碼會四散,編譯器也無法抓出「哪個回呼沒有接上」。

Delegate 未實作必要方法會造成編譯錯誤;Closure 屬性則可能維持 nil,安靜地略過。

第二,設定位置之間的距離。 Closure 的優點是,消費事件的程式碼會緊貼在觸發事件的呼叫旁邊。

開啟選擇器的程式碼和接收結果的程式碼位於三行之內,因此流程一目了然。Delegate 的設定(delegate = self)與實作(extension)則分散在檔案不同位置。

對一次性的互動來說,這套儀式過於繁瑣。網路要求完成、警示按鈕回應、動畫結束等「發生一次就結束」的事件,之所以以 Closure 為標準,原因就在此。

第三,狀態與身分。 Delegate 方法依慣例會將發送者放在第一個參數(imagePicker(_:didSelect:)picker)。

當一個畫面使用兩個 table view 時,能分辨事件來自哪一個,就是靠這項慣例。

Closure 不會自動傳遞發送者,因此在相同情況下,必須接上兩組 Closure,或直接在參數中設計發送者。

第四,記憶體管理陷阱的位置。 兩者都有循環參考風險,但陷阱的形狀不同。

Delegate 在宣告時以weak var delegate一次判斷即可,而且慣例很強,因此較少出錯。Closure 則必須在每個接入點重複判斷是否使用[weak self]。

正如 ARC(Automatic Reference Counting,自動參考計數)篇所整理的,沒有所有權環的一次性執行不需要 weak,但儲存在屬性中的回呼可能形成循環。

也就是說,每個使用位置都要重新做這項判斷。Closure 更容易暴露出錯的表面。

第五,測試與重用。 Closure 在測試中很輕量。

不需要 Mock 物件,只要在測試本文直接接上回呼並驗證是否被呼叫即可。Delegate 則需要建立測試用 Spy 類別,會增加準備程式碼。

不過,Delegate 協定也扮演「如何與這個元件通訊」的文件角色。多個畫面重用同一元件時,契約明確是一項優點。

以會議室的契約與櫃檯的便條比喻 Delegate 和 Closure 的比較插圖
Delegate 是會議室的契約,Closure 是櫃檯的便條

選擇判準 — 依事件數量、存續時間與方向決定

了解差異後,將它濃縮成選擇判準。大多數情況只要回答三個問題即可。

問題 1 — 有多少個事件? 一、兩個事件用 Closure;超過三個,或未來還會增加的關係用 Delegate。

事件群組越接近「對話」,契約(協定)的價值就越高。

問題 2 — 關係的存續時間如何? 像請求—回應一樣很快結束時用 Closure;只要畫面存在就持續往來的關係(捲動事件、文字編輯中的驗證)用 Delegate。

關係維持越久,單一 weak delegate 的安全性就越勝過在每個使用位置判斷[weak self]。

問題 3 — 是否需要取回值? Delegate 方法可以有回傳值。

textField(_:shouldChangeCharactersIn:)就是代表例。「是否可以執行」這種詢問式通訊,是 Delegate 的主場。

Closure 也能設計回傳型別,但儲存的可選 Closure 要如何處理回傳值(nil 時的預設值是什麼?)會變得不自然。

當然也有界線模糊的情況。這時可以參考 Apple 最近的方向。

以 UIAction 為基礎的按鈕處理器,以及UICollectionViewDiffableDataSource的 Closure provider 都是例子。遷移至 async/await(錯誤處理篇提到的 completion handler 世代交替)也是同樣的趨勢。

一次性、提供資料的通訊正持續移向 Closure 與 async。

相對地,持續互動的標準仍是 Delegate。UITableViewDelegate、UINavigationControllerDelegate 都是例子。

框架中的這種分工,與上面的三個問題完全一致。

只指出一個反模式:在每個畫面重新建立只有一個方法的 Delegate 協定。

為單一事件經歷協定宣告、採用、weak 屬性、extension 四層儀式,多半是過度設計。這個位置用一行 Closure 就對了。

反過來,類別掛了六、七個 Closure 屬性,就是應該改用 Delegate 統整的訊號。

以事件數量、關係存續時間與回傳值三個問題分支的回呼選擇流程圖
事件數量、關係存續時間、回傳值,三個問題就能決定

總結

  • Delegate 與 Closure 是相同需求(「發生事情時通知我」)的兩種實作。差異的根源在於,是否將契約宣告為型別(協定),或將事件當成值來傳遞。
  • 五項實際差異:Delegate 具備事件擴充性並能偵測未實作項目;Closure 讓設定位置更具內聚性;辨識發送者依賴 Delegate 慣例;Closure 的記憶體陷阱表面較大;測試輕量性則 Closure 較佳。
  • 用三個問題選擇:事件是否有三個以上、關係是否長期存在、是否需要回傳值。三者皆否定時用 Closure,只要有一項明確符合就用 Delegate。
  • 單一方法的 Delegate 協定與五、六個 Closure 屬性,是彼此相反方向的過度設計訊號。

想了解兩種工具的細節,請延伸閱讀。Delegate 模式篇介紹與 Observer 的比較,以及 1:1 通訊的標準做法。

Closure 完整整理篇則整理了捕捉、weak self 與 escaping。

延伸閱讀