Swift協(xié)議與關(guān)聯(lián)類型
作者丨狐友技術(shù)團(tuán)隊(duì)
來源丨搜狐技術(shù)產(chǎn)品(ID:sohu-tech)
本文字?jǐn)?shù):9972字
預(yù)計閱讀時間:25分鐘
Swift協(xié)議與關(guān)聯(lián)類型
目錄
前言
問題
關(guān)聯(lián)協(xié)議的限制
使用關(guān)聯(lián)協(xié)議需要做泛型改造
使用關(guān)聯(lián)協(xié)議失去了動態(tài)類型派發(fā)的能力
關(guān)聯(lián)協(xié)議與泛型的關(guān)系
解決問題的方案
組合方案
添加泛型函數(shù)
為關(guān)聯(lián)類型添加約束
結(jié)語
參考
前言
在Swift語言當(dāng)中,泛型(Generic)和協(xié)議(Protocol)都是非常重要的語言特性。使用泛型讓你能根據(jù)自定義的需求,編寫出適用于任意類型的、靈活可復(fù)用的函數(shù)及類型。你可以避免編寫重復(fù)的代碼,而是用一種清晰抽象的方式來表達(dá)代碼的意圖;使用協(xié)議能夠讓你設(shè)計一個藍(lán)圖,遵循協(xié)議的具體類型,幫助你實(shí)現(xiàn)某一特定的任務(wù)或者功能的方法、屬性,特別是協(xié)議可以作為類型使用,使其具有了動態(tài)派發(fā)的能力;本文將討論Swift協(xié)議(Protocol)中特殊的關(guān)聯(lián)類型(Associated Types),它與泛型(Generic)有相似性和又有區(qū)別。為了簡化文字描述,后續(xù)將帶有關(guān)聯(lián)類型的協(xié)議(Protocol With Associated Types),簡稱為關(guān)聯(lián)協(xié)議;而把普通的不包含任何關(guān)聯(lián)類型的協(xié)議(Plain Protocol)簡稱為普通協(xié)議。
問題
我們將首先討論一個業(yè)務(wù)開發(fā)中的具體問題,定制UITabbar和UITabBarController。
圖1 定制UITabbar元素示意圖
有兩種TabBarItem類型,一種是SNSTabBarItem,其中ImageView是圖片類型;另一種是SNSTabBarLotItem,其中ImageView是LottieView,即動畫類型;為了通用化設(shè)計,統(tǒng)一屬性名稱和調(diào)用流程,我們考慮通過設(shè)計協(xié)議來解決這個問題。
協(xié)議代碼如下所示:
public protocol SNSTabBarItemProtocol {
var itemLabel:UILabel! { get }
associatedtype itemImageViewType:UIView
var itemImageView:itemImageViewType! { get }
//創(chuàng)建TabBarItem內(nèi)部UI元素
func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage?)
//.......
}
其中,使用了關(guān)鍵字associatedtype,定義了一個關(guān)聯(lián)類型;滿足實(shí)際使用中,不同類型的TabBarItem中ImageView類型的變化,同時,又對其增加的限制,要求ImageView必須是繼承UIView的子類。另外,定義了createElement函數(shù),它會在自定義的CustomTabBarController中被調(diào)用,不同類型item其內(nèi)部實(shí)現(xiàn)不同,滿足不同UI布局的定制需求。
實(shí)現(xiàn)協(xié)議的兩種TabBarItem類型:
//第一種,SNSTabBarItem
class SNSTabBarItem: UITabBarItem, SNSTabBarItemProtocol {
var itemLabel:UILabel!
var itemImageView:UIImageView! //靜態(tài)圖片類型
func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage? = UIImage()){
//......內(nèi)部實(shí)現(xiàn)不同
}
}
//第二種,SNSTabBarLotItem
class SNSTabBarLotItem: UITabBarItem, SNSTabBarItemProtocol {
var itemLabel:UILabel!
var itemImageView:HYLotSwitchView! //Lottie動畫類型
func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage? = UIImage()){
//......內(nèi)部實(shí)現(xiàn)不同
}
}
接下來,在自定義CustomTabBarController中創(chuàng)建所有item,并調(diào)用協(xié)議中定義的方法createElements;
// 創(chuàng)建自定義圖標(biāo)
func createCustomIcons(_ containers: [String:UIView]) {
guard let items = self.tabBar.items, !items.isEmpty,!containers.isEmpty else{
return
}
let barItemWidth: CGFloat = self.tabBar.bounds.size.width / CGFloat(items.count)
for item in items { //遍歷元素
//..........
if let item = item as? SNSTabBarItemProtocol { //錯誤,Protocol 'SNSTabBarItemProtocol' can only be used as a generic constraint because it has Self or associated type requirements
item.createElements(superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: self.tabBar.bounds.size.height), backgroundImage: nil)
//..............
}
//..........
}
}
代碼中if let item = item as? SNSTabBarItemProtocol這里會遇到一個致命問題Protocol 'SNSTabBarItemProtocol' can only be used as a generic constraint because it has Self or associated type requirements,這是什么原因呢?我們通過簡單分析這個錯誤提示,可以得出一些線索:帶有associatedtype的關(guān)聯(lián)協(xié)議只能修飾泛型,這與普通協(xié)議相比帶來了明顯的差異和使用限制。
關(guān)聯(lián)協(xié)議的限制
使用關(guān)聯(lián)協(xié)議需要做泛型改造
我們來回顧一下Swift官方文檔關(guān)于協(xié)議作為類型(Protocols as Types)的描述:
?Protocols as TypesProtocols don’t actually implement any functionality themselves. Nonetheless, you can use protocols as a fully fledged types in your code. Using a protocol as a type is sometimes called an existential type, which comes from the phrase “there exists a type T such that T conforms to the protocol”.
?You can use a protocol in many places where other types are allowed, including:
As a parameter type or return type in a function, method, or initializer As the type of a constant, variable, or property As the type of items in an array, dictionary, or other container
挑重點(diǎn)進(jìn)行說明,協(xié)議本身實(shí)際上并不實(shí)現(xiàn)任何功能,但是你可以在代碼中使用協(xié)議作為完善的類型。
這說明我們前面問題中的使用方法針對普通協(xié)議是正確的,而針對關(guān)聯(lián)協(xié)議就不再正確;從錯誤提示可以得到答案,Protocol 'xxx' can only be used as a generic constraint because it has Self or associated type requirements,關(guān)聯(lián)協(xié)議只能修飾泛型。
通過一個具體的例子來驗(yàn)證:
protocol Proto{
}
var delegate:Proto
這段代碼運(yùn)行正常,接下來改造一下,增加associatedtype關(guān)聯(lián)類型
protocol Proto{
associatedtype T
}
var delegate:Proto //Protocol 'Proto' can only be used as a generic constraint because it has Self or associated type requirements
改為關(guān)聯(lián)協(xié)議后就會出現(xiàn)與前面例子相似的錯誤,那么我們來引入泛型進(jìn)行修改。
protocol Proto{
associatedtype T
}
class C<T:Proto> {
var delegate:T
init() {
}
}
運(yùn)行正確,從這里我們可以得到結(jié)論,每一個從前使用普通協(xié)議的的地方,現(xiàn)在為了使用associatedtype,需要進(jìn)行改造,引入泛型,使用關(guān)聯(lián)協(xié)議修飾泛型參數(shù),就能夠避免產(chǎn)生錯誤。
使用關(guān)聯(lián)協(xié)議失去了動態(tài)類型派發(fā)的能力
但是,改造后class C變成一個泛型類,帶有泛型T,T遵循Proto協(xié)議,然后在C內(nèi)部,delegate的類型是T,也就是說原本一個普通的class類型,需要被改造成泛型class,很多時候這不是我們設(shè)計的本意,而存粹是為了支持使用associatedtype,不得不進(jìn)行的改造。這樣失去了dynamic dispatch動態(tài)類型派發(fā)的能力!
比如有一個數(shù)組,其內(nèi)部存儲的類型是不同的,但是遵循相同的協(xié)議,這在使用普通協(xié)議時,是可行的,而使用帶有associatedtype的關(guān)聯(lián)協(xié)議就不可行了,失去了動態(tài)派發(fā)的能力,多態(tài)的能力,只能變成一個統(tǒng)一的類型,而不能支持不同類型 ,因此我們失去了一個重要的語言特性。
關(guān)聯(lián)協(xié)議與泛型的關(guān)系
關(guān)聯(lián)協(xié)議,從外部看,使用associatedtype更像是提供了一個語法糖,提供有意義的名字做占位;從內(nèi)部看,建立類型的語意要求,使用typealias顯示或者隱式指明具體類型;利用associatedtype相當(dāng)于定義了一個未知類型的占位符,并且這個占位符可以在協(xié)議定義的整個生命周期內(nèi)使用。
我們來對比兩段代碼:
protocol Animal{
associatedtype Food
func eat(food:Food)
}
協(xié)議Animal定義了每種動物要eat某種類型的Food,到現(xiàn)在為止,我們還不知道哪種動物吃哪種Food;
struct Animal<Food>{
func eat(food:Food)
}
Animal結(jié)構(gòu)體,支持泛型參數(shù)Food,定義每種動物eat某種類型的Food;
這種場景下,使用關(guān)聯(lián)協(xié)議和使用泛型參數(shù)作用非常相似, 但是他們之間仍然不完全相同。
由于目前的語言限制,協(xié)議中無法使用泛型,我們假設(shè)可以使用泛型協(xié)議,寫出類似下面的代碼,然后與使用關(guān)聯(lián)協(xié)議的代碼進(jìn)行對比分析:
//假設(shè)泛型協(xié)議成立
protocol Animal<Food> {
func eat(food:Food)
}
struct Grass:Food{
}
struct Cow:Animal<Grass>{ //泛型參數(shù)指定具體遵循協(xié)議的類型
func eat(f: Grass) {
}
}
//使用關(guān)聯(lián)協(xié)議
protocol Animal{
associatedtype Food
func eat(food:Food)
}
struct Cow:Animal{
func eat(f: Food) {
Self.Food //通過類似屬性的方式,直接獲取到關(guān)聯(lián)類型的名字
}
}
從外部看,我們使用泛型協(xié)議方式,只能看到遵循協(xié)議的具體類型,即Grass是一個遵循Food協(xié)議的類型;對比使用associatedtype的關(guān)聯(lián)協(xié)議,我們可以通過類似屬性的方式,可以直接獲取到關(guān)聯(lián)類型的名字,這使得某些情況下,添加參數(shù)類型的約束限制成為可能。還不止于此,如果有多個關(guān)聯(lián)類型,或者關(guān)聯(lián)類型需要被其他關(guān)聯(lián)協(xié)議限制,或者同時使用多個協(xié)議,這些復(fù)雜的情況組合,就使得假設(shè)的泛型協(xié)議很難代替關(guān)聯(lián)協(xié)議,并且泛型協(xié)議不得不把這些(原本可以通過associatedtype隱藏在內(nèi)部的)信息全部暴露給外部使用者。
另外,關(guān)聯(lián)協(xié)議利用associatedtype解決的問題是面向?qū)ο蟮念愋完P(guān)系繼承,來看下面例子:
protocol Food{
}
struct Grass:Food{
}
protocol Animal {
func eat(f:Food)
}
struct Cow:Animal {
func eat(f: Grass) { //Type 'Cow' does not conform to protocol 'Animal'
}
}
首先定義了Food協(xié)議,Grass作為一種具體的食物遵循Food協(xié)議;另外,我們通過Animal協(xié)議,規(guī)范動物需要eat食物Food,具體是哪種Food沒有確定,最后Cow作為一種具體的動物,遵循Animal協(xié)議,實(shí)現(xiàn)了eat方法,參數(shù)指定Grass類型,Grass遵循Food協(xié)議,但是編譯器提示錯誤,Cow沒有遵循Animal協(xié)議,只能改為func eat(f: Food);
我們可以發(fā)現(xiàn):遵循普通協(xié)議的具體類型,其內(nèi)部遵循的協(xié)議類型不能捕獲復(fù)雜的類型關(guān)系
接下來改造Animal協(xié)議為關(guān)聯(lián)協(xié)議
protocol Food{
}
struct Grass:Food{
}
protocol Animal {
associatedtype FoodType //關(guān)聯(lián)類型
func eat(f:FoodType)
}
struct Cow:Animal {
func eat(f: Grass) { //Grass遵循Food協(xié)議,OK
}
}
Cow().eat(f: Grass())
有了associatedtype的幫助,可以完成面向?qū)ο蟮念愋屠^承關(guān)系使用。
解決問題的方案
現(xiàn)在我們討論文章開頭提出的的問題如何解決,有兩種方案可供參考:
組合方案
typealias Codable = Decodable & Encodable
我們經(jīng)常使用Codable協(xié)議進(jìn)行數(shù)據(jù)序列化,這里可以參考Codable的設(shè)計模式,采用組合方案;
把SNSTabBarItemProtocol協(xié)議拆分成兩個協(xié)議:
//協(xié)議只包含需要遵守的屬性
public protocol SNSTabBarItemElements{
var itemLabel:UILabel! { get }
associatedtype itemImageViewType:UIView
var itemImageView:itemImageViewType! { get }
}
//協(xié)議只包含需要遵守的方法
public protocol SNSTabBarItemFunctions{
//創(chuàng)建TabBarItem內(nèi)部UI元素
func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage?)
}
//協(xié)議組合
public protocol SNSTabBarItemProtocol: SNSTabBarItemElements & SNSTabBarItemFunctions {
}
經(jīng)過這樣改造之后,我們修改調(diào)用處協(xié)議:
// 創(chuàng)建自定義圖標(biāo)
func createCustomIcons(_ containers: [String:UIView]) {
//..........
for item in items { //遍歷元素
//..........
if let item = item as? SNSTabBarItemFunctions { //OK,不再報錯,避開了關(guān)聯(lián)協(xié)議問題
item.createElements(superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: self.tabBar.bounds.size.height), backgroundImage: nil)
//..............
}
//..........
}
}
原來的轉(zhuǎn)換為SNSTabBarItemProtocol協(xié)議的方式,更改為使用SNSTabBarItemFunctions這個子協(xié)議,而兩個具體的UITabBarItem子類仍然遵循SNSTabBarItemProtocol協(xié)議,保持不變;這樣通過組合的方式,我們繞開了關(guān)聯(lián)協(xié)議只能修飾泛型的問題,把它變成了當(dāng)前場景下只使用普通協(xié)議,調(diào)用協(xié)議內(nèi)限定的函數(shù);
添加泛型函數(shù)
既然關(guān)聯(lián)協(xié)議只能用作泛型約束,因?yàn)樗嘘P(guān)聯(lián)類型要求,那么我們是否可以選擇另一個方案:創(chuàng)造一個泛型函數(shù),封裝createElements的調(diào)用并添加參數(shù)的泛型約束,我們來試試:
//item改為泛型參數(shù),遵守SNSTabBarItemProtocol協(xié)議
func loopElements<E:SNSTabBarItemProtocol >(item:E,superView:UIView,position: CGRect,backgroundImage: UIImage?){
item.createElements(superView: superView, position: position, backgroundImage: backgroundImage)
}
//遍歷元素內(nèi)部使用loopElements方法
func createCustomIcons(_ containers: [String:UIView]) {
//..........
for item in items { //遍歷元素
//..........
loopElements(item: item, superView: container, position: CGRect(x: CGFloat(index) * barItemWidth, y: 0, width: barItemWidth, height: 0), backgroundImage: nil) //錯誤,Global function 'loopElements(item:superView:position:backgroundImage:)' requires that 'UITabBarItem' conform to 'SNSTabBarItemProtocol'
//..........
}
}
修改遍歷元素內(nèi)部的代碼,調(diào)用loopElements泛型函數(shù),確實(shí)滿足了關(guān)聯(lián)協(xié)議的要求。
但是,新的問題會產(chǎn)生,調(diào)用loopElements會提示 Global function 'loopElements(item:superView:position:backgroundImage:)' requires that 'UITabBarItem' conform to 'SNSTabBarItemProtocol' ,這是因?yàn)閠abbar中的items數(shù)組元素,類型只能是UITabBarItem,不能添加SNSTabBarItemProtocol的限制,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(244, 138, 0);">SNSTabBarItemProtocol是關(guān)聯(lián)協(xié)議,只能修飾泛型,items中數(shù)組元素不是泛型,因此,這個方案不能繼續(xù)推進(jìn)成功。
為關(guān)聯(lián)類型添加約束
接下來我們跳出問題本身,繼續(xù)討論為關(guān)聯(lián)協(xié)議中的關(guān)聯(lián)類型添加約束;它可以進(jìn)一步來要求遵循的類型滿足約束,這在很多場景下具有很多實(shí)際價值,能夠抽象代碼避免重復(fù)。
例如,下面的代碼定義了MySequence協(xié)議,MySequence協(xié)議遵循Comparable協(xié)議,其中的關(guān)聯(lián)類型Element也遵循Comparable協(xié)議。
protocol MySequence: Comparable {
associatedtype Element: Comparable
var storage: [Element] { get set }
}
由于對 Element 添加了協(xié)議限制,Comparable 協(xié)議需要實(shí)現(xiàn)的比較方法就可以給出實(shí)現(xiàn);
extension MySequence {
static func < (lhs: Self, rhs: Self) -> Bool {
for (left, right) in zip(lhs.storage, rhs.storage) {
if left < right {
return true
}
}
return false
}
}
另外,我們也可以在關(guān)聯(lián)類型約束里使用協(xié)議,用 where 從句實(shí)現(xiàn)更復(fù)雜的約束;
protocol MySequence: Comparable {
associatedtype Element: Comparable
associatedtype Slice: MySequence where Slice.Element == Element
var storage: [Element] { get set }
}
這里協(xié)議可以作為它自身的要求出現(xiàn),即Slice擁有兩個約束,它必須遵循 MySequence 協(xié)議,同時它的Element的類型必須是和storage數(shù)組中元素Element類型相同。
我們也可以為關(guān)聯(lián)類型添加默認(rèn)值,如下面所示Element默認(rèn)為Int類型:
protocol MySequence: Comparable {
associatedtype Element: Comparable = Int
var storage: [Element] { get set }
}
并且可以為為默認(rèn)的 Associated Type 提供方法的默認(rèn)實(shí)現(xiàn)。
protocol MySequence4: Comparable {
associatedtype Element: Comparable = Int
var storage: [Element] { get set }
func summed() -> Element
}
Element 現(xiàn)在默認(rèn)是 Int,接下來通過extension給出函數(shù)summed的默認(rèn)實(shí)現(xiàn)。
extension MySequence {
func summed() -> Element {
return storage.reduce(0, +) as! Self.Element //Cannot convert value of type '(Int) -> Int' to expected argument type '(Int, Self.Element) throws -> Int'
}
}
但是此處會提示錯誤,無法推斷出默認(rèn)類型是Int,即 extension 中的 Element 只受“約束”的影響,即只受 Comparable 和 where 從句的影響,并沒有接受默認(rèn)值。所以我們需要針對extension增加限制。
extension MySequence where Element == Int {
func summed() -> Element {
return storage.reduce(0, +)
}
}
只有滿足Element類型是Int的,才能使用summed的默認(rèn)實(shí)現(xiàn)。這樣就可以保證準(zhǔn)確。
結(jié)語
本文從業(yè)務(wù)場景的實(shí)例出發(fā),詳細(xì)討論了使用關(guān)聯(lián)類型的協(xié)議可能會出現(xiàn)的問題,并且對比了與普通協(xié)議的不同;我們可以看到關(guān)聯(lián)協(xié)議更類似范型參數(shù);如果要使用關(guān)聯(lián)類型的協(xié)議,就必須進(jìn)行范型改造,這種方式使得類型失去了動態(tài)派發(fā)的能力,需要根據(jù)具體情況合理選擇。另外,我們也詳細(xì)介紹了如何為關(guān)聯(lián)類型添加約束,通過添加約束可以實(shí)現(xiàn)更復(fù)雜的要求,如添加默認(rèn)類型和默認(rèn)類型的方法實(shí)現(xiàn),優(yōu)化代碼設(shè)計方式,避免重復(fù)。
參考
-
https://betterprogramming.pub/swift-protocols-with-associated-types-and-generics-373b2927baed -
https://zhuanlan.zhihu.com/p/80672557 -
https://www.hackingwithswift.com/example-code/language/how-to-fix-the-error-protocol-can-only-be-used-as-a-generic-constraint-because-it-has-self-or-associated-type-requirements
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
