<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Swift協(xié)議與關(guān)聯(lián)類型

          共 17127字,需瀏覽 35分鐘

           ·

          2022-05-30 09:07

          ????關(guān)注后回復(fù) “進(jìn)群” ,拉你進(jì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 SNSTabBarItemUITabBarItemSNSTabBarItemProtocol {
              var itemLabel:UILabel!
              var itemImageView:UIImageView! //靜態(tài)圖片類型
              
              func createElements(superView: UIView, position: CGRect, backgroundImage:UIImage? = UIImage()){
                  //......內(nèi)部實(shí)現(xiàn)不同
              }
          }
          //第二種,SNSTabBarLotItem
          class SNSTabBarLotItemUITabBarItemSNSTabBarItemProtocol {
              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 asSNSTabBarItemProtocol {  //錯誤,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 SNSTabBarItemProtocolSNSTabBarItemElements & SNSTabBarItemFunctions {
          }

          經(jīng)過這樣改造之后,我們修改調(diào)用處協(xié)議:

          // 創(chuàng)建自定義圖標(biāo)    
          func createCustomIcons(_ containers: [String:UIView]) {
              //..........
              for item in items {   //遍歷元素
                  //..........
                  if let item = item asSNSTabBarItemFunctions {  //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 MySequenceComparable {
              associatedtype ElementComparable
              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 (leftrightin zip(lhs.storage, rhs.storage) {
                      if left < right {
                          return true
                      }
                  }
                  return false
              }
          }

          另外,我們也可以在關(guān)聯(lián)類型約束里使用協(xié)議,用 where 從句實(shí)現(xiàn)更復(fù)雜的約束;

          protocol MySequenceComparable {
              associatedtype ElementComparable
              associatedtype SliceMySequence 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 MySequenceComparable {
              associatedtype ElementComparable = Int
              var storage: [Element] { get set }
          }

          并且可以為為默認(rèn)的 Associated Type 提供方法的默認(rèn)實(shí)現(xiàn)。

          protocol MySequence4Comparable {
              associatedtype ElementComparable = 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, +) asSelf.Element //Cannot convert value of type '(Int) -> Int' to expected argument type '(Int, Self.Element) throws -> Int'
              }
          }

          但是此處會提示錯誤,無法推斷出默認(rèn)類型是Int,即 extension 中的 Element 只受“約束”的影響,即只受 Comparablewhere 從句的影響,并沒有接受默認(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)盤了,歡迎下載!

          點(diǎn)擊??卡片,關(guān)注后回復(fù)【面試題】即可獲取

          在看點(diǎn)這里好文分享給更多人↓↓

          瀏覽 22
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕亚洲第一页在线 | 日韩精品一区二区亚洲AV观看 | 五月色丁香 | 成人激情视频网 | 青青草原视频免费在线观看 |