Swift. Delegation

讓我們理解 Delegation 是怎麼一回事吧!

Jeremy Xue
Jeremy Xue ‘s Blog

--

Photo by Graziano De Maio on Unsplash

▸ 前言:

委託(Delegation)是一種在 iOS 開發中常見的設計模式,而加上 iOS 又屬於協定導向程式設計(Protocol Oriented Programming,POP),常常會透過 dataSource 或 delegate 這種委託方式來處理畫面呈現以及事件傳遞,因此是一種必須要掌握的技術。

而這篇文章我會分享我認為的 Delegation 概念以及常見的問題,之後會再透過另一篇文章介紹更多不同的 Delegation 用法。

▸ 常見的迷思

我相信許多人第一次碰到 delegation(可能是 delegate)是在所謂的「資料回傳」或是「頁面傳值」。舉個最常見的例子,頁面互傳。如下圖:

常見的 Delegation 範例

當我們要從 PageA -> PageB 傳值時,我們可以透過在實例化 PageB 的物件之後,透過類似 PageB.value = 1 的方式直接修改 PageB 的資料。但是,反之,我們要從 PageB 傳值回 PageA 時,我們就無從下手了(因為不會在實例化一個 PageA 出來)。

那就用 Delegation 解決吧!?

其實我覺得這算是一種迷思。對我來說,因為 Delegation 只是解決傳值問題的其中一個方案,而不是絕對。你可以透過以下幾種常見的方式進行傳值:

  • Closure
  • Reference
  • Delegation(也算一種 Reference)
  • Notification、Observer

而另一個迷思則是「Delegation 一定要配合 Protocol 使用」,我認爲這部分以結果論來說這樣做比較好,但某部分來說不一定需要。我認為的 Delegation 就如同字面上的意思 — 委託。所以讓某個特定物件幫你處理某個特定事件時就算是委託,我們可以參考 wiki 上面的範例:

在委派模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委派給另一個對象來處理。

接著,我們先看常見的 Delegation 程式碼範例如下:

但其實我們可以不使用 protocol 就能達成相同操作,我們將 PageB 中的 delegate 的型別改為 PageA 即可:

由此可見,你應該可以了解 Delegation 本質上是使用 reference 的概念,而透過 protocol 的方式只是多了一個抽象的過程而已。

▸ 為什麼需要配合 Protocol

那為什麼通常 Delegation 都配合 Protocol 作為使用呢?從我們上面的範例看得出來,多了一個「抽象」的過程」,然而這有什麼用途呢?

這邊我們舉一個餐廳的範例,我們希望裡面有一個有煮飯的能力的員工,在不考慮任何情況下,我們的程式碼可能如下 :

這邊我們明確限制了員工的類型為 Employee,而 Employee 中有 cook 方法讓我們能夠進行 Restaurantcook 的操作。我們可以這樣解釋這段程式碼:

我們的餐廳可能有一個員工,並且我可以叫他煮飯(但這邊程式碼看起來比較像是「只要是員工,都會煮飯)

因為這邊我們明確標記員工的類型為 Employee ,所以,他無法被抽換為其他類別。但我們只是單純需要的只是一個有「煮飯的能力」的人,因此我們不應該限制他的類型(員工可以煮飯沒錯,但其他人也可能可以煮飯)。當你限制類型之後,之後再抽換類型會比較麻煩。

假設我們今天餐廳會有一個實習生,並且他也可以暫時代替員工幫忙煮飯,你的程式碼可能會這樣調整:

以上的兩個方案都可以解決這個問題,但你都需要修改原有的類型或是額外宣告內容。但我們想要的效果應該是「餐廳」有煮飯的功能,而這個功能只要是有「煮飯的能力」的人都可以(員工、實習生等等)。

左圖)
我們的餐廳可能有一個員工或實習生(都有/都沒有),我可以叫他們煮飯
右圖)
我們的餐廳可能有一個員工,並且我可以叫他煮飯
(只要是員工或實習生,都會煮飯)

因此這邊我們可以透過 protocol 來幫我們實現這個操作。首先,我們先建立一個 Cookable protocol,並且遵循此協議的物件必須要實現 cook 方法。(我們要求會煮飯的人,必須要有 cook 方法):

因此我們 Restaurant 中的程式碼可以調整下圖,我們刪除所有屬性,只有添加一個 someoneCanCook 變數,並且其類型為 Cookable

我們的餐廳可能有一個有煮飯能力的人,並且我可以叫他煮飯

接著我們就能夠任意抽換 someoneCanCook 中的物件(只要遵循 Cookable 協議),他們會執行相對應物件的 cook 方法的內容,並且不需要調整任何內容:

由此可見,假設我們把這個概念應用到常見的頁面間傳值上。你就知道為什麼每個範例的 Delegation 方式都會配合 Protocol 使用,因為只要單純描述是一個能夠接收資訊的人,而不是定義為某個明確的頁面或物件,如此一來,在抽換物件上或是進行重用時更為方便,增加程式碼的靈活度。

▸ 避免強引用循環

在我們使用 delegation 的時候,常常會出現 xxx.delegate = self 之類的程式碼,所以在使用上我們會飲用這些物件,所以必須考慮到可能有強引用循環的問題。這邊我們使用上面的範例,並在 PageA 中加入反初始化器:

接著我們運行以下的程式碼:

可以發現我們在將 pageA = nil 之後,我們呼叫 pageB.sendValueToPageA 依然能夠印出訊息,因為我們 pageA 實例還沒真正的被釋放掉(pageB.delegate 還持有對它的引用),所以在我們將 pageB.delegate 設為 nil 後,再次呼叫方法就不會印出任何訊息了(該 PageA 實例已被釋放),並且可以看見 PageA 中反初始化器所印出的訊息。

因此我們應該將 PageB 中的 delegate 屬性加上 weak 修飾,讓他對於 delegation 的物件保持弱引用來避免強引用循環。要做到這一點,我們必須在我們的協議之後加上 AnyObject(或 class,但將來可能棄用),讓此協議只能被 class 所遵循。

關於 AnyObject 或 class 的選擇,可以參考這篇文章:
https://forums.swift.org/t/class-only-protocols-class-vs-anyobject/11507/12

接著我們程式碼會變為以下這樣:

我們一樣執行與上述一樣的過程,但在我們 pageA = nil 的時候,該 PageA 實例也同時被釋放,之後無論怎麼呼叫 pageB.delegate 方法都不會再印出訊息。

▸ Delegation 對象

我們應該常常會看見 xxx.delegate = self 或是 xxx.dataSource = self ,但這也算是一種迷思,因為既然都是稱為「委託」模式了,我們也可以再將它委託給別人處理。舉個常見的 tableView.dataSource 的使用方式:

但其實這邊我們也可以額外寫一個物件,並讓他遵循 UITableViewDataSource 並且實作所需的方法即可。如此一來我們也可以得到相同的結果。

所以在使用 delegation 相關操作時,不一定是 delegate = self ,有時候你可以委託給一個物件,甚至使用透過 delegation 的方式交給另一個 delegation 去處理也是可行的。

所以,你可以根據情況使用最符合需求的委託方式,不一定都只能為 self。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]