Swift中的協議

weixin_34107955發表於2017-05-25
  • 定義: 協議定義了一個藍圖,規定了用來實現某一特定的任務或者功能的方法、屬性,或其他需要的東西。
  • 類、結構體、列舉都可以遵循協議,併為協議定義的這鞋要求提供具體實現。某個型別能滿足某個協議的要求,就可以說該型別遵循這個協議。
  • 除了遵循協議的型別必須實現的要求外,還可以對協議進行擴充套件,通過擴充套件來實現一部分要求或者實現一些附加功能,這樣遵循協議的型別就能夠使用這些功能。

協議語法

  • 協議定義方式和類、結構體和列舉的定義方式類似:
protocol SomeProtocol {
  // 這裡是協議的定義部分
}
  • 自定義型別遵循某個協議時,要在型別名稱後加上些協議名稱,中間用冒號:分割。遵循多個協議是,各個協議之間用逗號,隔開:
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 這裡是結構體的定義部分
}
  • 當擁有父類在遵循協議時,用該將父類放在協議名之前,用逗號,隔開:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 這裡是類的定義部分
}

屬性協議

  • 協議可以要求遵循協議的型別提供特定名稱和型別的例項屬性或型別屬性。協議不指定屬性是儲存型屬性還是計算型屬性,它只指定屬性的名稱和型別。此外,協議還指定屬性是可讀的還是可讀可寫的。
  • 如果協議要求屬性是可讀可寫的,那麼該屬性不能是常量屬性或只讀的計算型屬性。如果協議只要求屬性是可讀的,那麼該屬性不僅可以是可讀的,如果程式碼需要的話,還可以是可寫的。
  • 協議總是用 var 關鍵字來宣告變數屬性,在型別宣告後加上 { set get } 來表示屬性是可讀可寫的,可讀屬性則用 { get } 來表示:
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

當協議中定義型別屬性時,總是使用static關鍵字作為字首。當類型別遵循協議時,除了static,還可以用class關鍵字來生命類型別屬性:

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

如下所示,這是一個只含有一個例項屬性要求的協議:

protocol FullNamed{
  var fullNamed: String{ get }
}

FullName協議除了要求遵循協議的型別提供fullname屬性之外,並無其他要求。
下面是一個遵循FullNamed協議的一個結構體:

struct Person: fullNamed{
  var fullNamed: string
}
let john = Person(fullName: "John Appleseed")

Person結構體中每一個例項都有一個叫fullnamed的儲存型屬性。

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}

Starship來吧fullname屬性實現為只讀的計算型屬性。

方法協議

  • 協議可以要求遵循協議的型別實現某些指定的例項方法類方法。這些方法作為協議的一部分,像普通方法一樣放在協議的定義中,但是不需要大括號和方法體。可以在協議中定義具有可變引數的方法,和普通方法的定義方式相同。但是,不支援為協議中的方法的引數提供預設值。
  • 在協議中定義類方法的時候,總是使用static關鍵字作為字首。當類型別遵循協議時,也可以使用class關鍵字作為字首。
protocol SomeProtocol {
    static func someTypeMethod()
}

下面的協議之定義了一個例項方法:

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 協議要求遵循協議的型別必須要實現一個名為random,返回值為Double型別的例項方法。RandomNumberGenerator協議並不關心具體的方法實現

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c) % m)
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 列印 “Here's a random number: 0.37464991998171”
print("And another one: \(generator.random())")
// 列印 “And another one: 0.729023776863283”

Mutating協議要求

  • 作用:在方法中改變方法所屬的例項
  • 在值型別(即結構體和列舉)的例項方法中,將 mutating 關鍵字作為方法的字首,寫在 func 關鍵字之前,表示可以在該方法中修改它所屬的例項以及例項的任意屬性的值。
  • 如果你在協議中定義了一個例項方法,該方法會改變遵循該協議的型別的例項,那麼在定義協議時需要在方法前加 mutating 關鍵字。這使得結構體和列舉能夠遵循此協議並滿足此方法要求。
  • 實現協議中的 mutating 方法時,若是類型別,則不用寫 mutating 關鍵字。而對於結構體和列舉,則必須寫 mutating 關鍵字。

如下所示:

protocol Togglable {
    mutating func toggle()
}

定義了一個只有一個toggle的例項方法,這個方法允許修改例項屬性的值。
當使用列舉或者結構體來實現Togglable協議時,需要實現一個字首為mutating關鍵字的toggle方法。

下為一個OnOffSwitch的列舉。有開、關兩種狀態,用列舉成員On和Off表示。toggle方法字首為mutating滿足Togglable協議要求。

enum OnOffSwitch: Togglable {
    case Off, On
    mutating func toggle() {
        switch self {
        case Off:
            self = On
        case On:
            self = Off
        }
    }
}
var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()
// lightSwitch 現在的值為 .On

構造器協議

協議可以要求遵循協議的型別實現指定的構造器。

protocol SomeProtocol{
  init(somePara:Int)
}

必要構造器

在遵循協議的類中實現構造器,無論是作為指定構造器,還是作為便利構造器。無論哪種情況,你都必須為構造器實現標上 required 修飾符:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 這裡是構造器的實現部分
    }
}

使用required修飾符可以確保所有子類也必須提供構造器實現,從而也能符合協議。

如果類已經被標記為 final,那麼不需要在協議構造器的實現中使用 required 修飾符,因為 final 類不能有子類。關於 final 修飾符的更多內容,請參見防止重寫。

如果一個子類重寫了父類的指定構造器,並且該構造器滿足了某個協議的要求,那麼該構造器的實現需要同時標註 required 和 override 修飾符:

protocol SomeProtocol {
    init()
}
 
class SomeSuperClass {
    init() {
        // 這裡是構造器的實現部分
    }
}
 
class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因為遵循協議,需要加上 required
    // 因為繼承自父類,需要加上 override
    required override init() {
        // 這裡是構造器的實現部分
    }
}

可失敗構造器

  • 協議還可以遵循協議的型別定義可失敗構造器。
  • 遵循協議的型別可以通過可失敗構造器(init?)或非可失敗構造器(init)來滿足協議中定義的可失敗構造器要求。協議中定義的非可失敗構造器要求可以通過非可失敗構造器(init)或隱式解包可失敗構造器(init)來滿足。

協議作為型別

  • 儘管協議本身並未實現任何功能,但是協議可以被當做一個成熟的型別來使用。

協議可以像其他普通型別一樣使用,使用場景如下:

  • 作為函式、方法或構造器中的引數型別或者返回值型別
  • 作為常量、變數或屬性的型別
  • 作為陣列、字典或其他容器的元素型別

協議是一種型別,因此協議型別的名稱應與其他型別(例如 Int,Double,String)的寫法相同,使用大寫字母開頭的駝峰式寫法,例如(FullyNamed 和 RandomNumberGenerator)。

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

上面定義了一個Dice類,代表桌球中擁有N個面的?。Dice建立的例項包含sides和generator兩個屬性,前者代表有幾個面,後者為?提供一個隨機數生成器,生成隨機點數。

generator屬性型別為RandomNumberGenerator,因此所有遵循RandomNumberGenerator協議的型別的例項都可以賦值給generator,除此之外沒有其他要求。

Dice 類還有一個構造器,用來設定初始狀態。構造器有一個名為 generator,型別為 RandomNumberGenerator 的形參。在呼叫構造方法建立 Dice 的例項時,可以傳入任何遵循 RandomNumberGenerator 協議的例項給 generator。

Dice 類提供了一個名為 roll 的例項方法,用來模擬骰子的面值。它先呼叫 generator 的 random() 方法來生成一個 [0.0,1.0) 區間內的隨機數,然後使用這個隨機數生成正確的骰子面值。因為 generator 遵循了 RandomNumberGenerator 協議,可以確保它有個 random() 方法可供呼叫。

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

代理模式

  • 代理是一種設計模式,它允許類或結構體將一些需要它們負責的功能委託給其他型別的例項。
  • 定義:定義協議來封裝那些需要被委託的功能,這樣就能確保遵循協議的型別能提供這些功能。
  • 代理模式可以用來相應特定的動作,或者接受外部資料來源提供的資料,為不用關心外部資料來源的型別。

通過擴充套件新增協議一致性

在無法修改原始碼的情況下,依然可以通過擴充套件令已有型別遵循並符合協議。擴充套件可以為已有型別新增屬性、方法、下標以及構造器,可以符合協議中的要求。

通過擴充套件使已有型別遵循並符合協議時,該型別的所有勢力也會隨之獲得協議中定義的各項功能。

舉個?,下面有個TextRepresentable協議,任何想要通過文字表達一些內容的型別都可以實現該協議。

protocol TextRepresentable {
   var textualDescription: String { get }
}

可以通過擴充套件,可以使Dice類遵循協議實現協議方法:

extension Dice: TextRepresentable {
   var textualDescription: String {
       return "A \(sides)-sided dice"
   }
}

通過擴充套件遵循並符合協議,和在原始定義中遵循並符合協議的效果完全相同。協議名稱寫在型別名之後,以冒號隔開,然後在擴充套件的大括號內實現協議要求的內容。

通過擴充套件遵循協議

當一個類已經符合了某個協議中的所有要求,卻沒有生命遵循該協議時,可以通過空擴充套件體的擴充套件來遵循該協議:

struct Hamster {
    var name: String
       var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 列印 “A hamster named Simon

注意:即使滿足了協議的所有要求,型別也不會自動遵循協議,必須顯式地遵循協議。

協議型別的集合

協議型別可以在陣列或者字典這樣的集合中使用。

下面建立了一個元素型別為TextRepresentable的陣列:

let things: [TextRepresentable] = [game, d12, simonTheHamster]

通過遍歷things陣列,列印每個元素的文字表示:

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice

thing 是 TextRepresentable 型別而不是 Dice,DiceGame,Hamster 等型別,即使例項在幕後確實是這些型別中的一種。由於 thing 是 TextRepresentable 型別,任何 TextRepresentable 的例項都有一個 textualDescription 屬性,所以在每次迴圈中可以安全地訪問 thing.textualDescription。

協議的繼承

協議能夠繼承一個或者多個其他協議,可以在繼承的協議的基礎上增加新的要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號隔開:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 這裡是協議的定義部分
}

類型別專屬協議

通過新增 class 關鍵字來限制協議只能被類型別遵循,而結構體或列舉不能遵循該協議。class 關鍵字必須第一個出現在協議的繼承列表中,在其他繼承的協議之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 這裡是類型別專屬協議的定義部分
}

協議 SomeClassOnlyProtocol 只能被類型別遵循。如果嘗試讓結構體或列舉型別遵循該協議,則會導致編譯錯誤。

協議合成

有時需要遵循多個協議,可以將多個協議採用SomeProtocol & AnotherProtocol 這樣的格式進行組合,成為協議合成
可以羅列任意多個你想要遵循的協議,以與符號(&)分隔。
舉個?:

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// 列印 “Happy birthday Malcolm - you're 21!

這段程式碼中宣告瞭Named和Aged協議,一個遵循了這兩個協議的結構體Person,Person並實現了這兩個協議。

wishHappyBirthday(to:) 函式的引數 celebrator 的型別為 Named & Aged。這意味著它不關心引數的具體型別,只要引數符合這兩個協議即可。

協議合成並不會生成新的、永久的協議型別,而是將多個協議中的要求合成到一個只在區域性作用域有

檢查協議一致性

使用型別轉換中描述的 is 和 as 操作符來檢查協議一致性,即是否符合某協議,並且可以轉換到指定的協議型別。檢查和轉換到某個協議型別在語法上和型別的檢查和轉換完全相同:

  • 使用型別轉換中描述的 is 和 as 操作符來檢查協議一致性,即是否符合某協議,並且可以轉換到指定的協議型別。檢查和轉換到某個協議型別在語法上和型別的檢查和轉換完全相同:
  • is 用來檢查例項是否符合某個協議,若符合則返回 true,否則返回 false。
  • as? 返回一個可選值,當例項符合某個協議時,返回型別為協議型別的可選值,否則返回 nil。

舉個?吧:定義一個HasArea的協議,定義了一個只讀屬性area

protocol HasArea{
  var area: Double{get}
}

定義一個Circle和一個Country類,都遵循了HasArea協議:

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

最後定義一個Animal類,但是沒有遵循HasArea協議:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs
    }
}

Circle,Country,Animal 並沒有一個共同的基類,儘管如此,它們都是類,它們的例項都可以作為 AnyObject 型別的值,儲存在同一個陣列中:

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

最後當要對objects陣列進行迭代,並對每一個元素進行檢查,看它是否符合HasArea協議:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

當迭代出的元素符合HasArea協議時,將as?操作符返回的可選值通過可選繫結,繫結到objectWithArea常量上。

objects陣列中的元素型別並不會因為強轉而丟失型別資訊,仍然是Circle,Country,Animal型別。當被賦值給objectWithArea時,只被視為HasArea型別,因此只有area屬效能夠被訪問。

可選的協議要求

  • 協議可以定義可選要求,遵循協議的型別可以選擇是否實現這些要求。在協議中使用optional關鍵字作為字首來定義可選要求。可選要求用在需要和OC打交道的程式碼中。協議和可選要求都必須帶上@objc屬性。標記@objc特性的協議只能被繼承自OC類的類或者@objc類遵循,其他類以及結構體和列舉不能遵循這種協議。
  • 使用可選屬性或者方法時,它們的型別會自動程式設計可選的。比如一個(Int)->String的方法會變成((Int)->String)?。注意:是整個函式型別是可選的,而不是函式的返回值。

下面的例子定義了一個名為 Counter 的用於整數計數的類,它使用外部的資料來源來提供每次的增量。資料來源由 CounterDataSource 協議定義,包含兩個可選要求:

@objc protocol CounterDataSource {
    optional func incrementForCount(count: Int) -> Int
    optional var fixedIncrement: Int { get }
}

嚴格來講,CounterDataSource 協議中的方法和屬性都是可選的,因此遵循協議的類可以不實現這些要求,儘管技術上允許這樣做,不過最好不要這做。

Counter 類含有 CounterDataSource? 型別的可選屬性 dataSource,如下所示:

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.incrementForCount?(count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

Counter 類使用變數屬性 count 來儲存當前值。該類還定義了一個 increment 方法,每次呼叫該方法的時候,將會增加 count 的值。

increment() 方法首先試圖使用 increment(forCount:) 方法來得到每次的增量。increment() 方法使用可選鏈式呼叫來嘗試呼叫 increment(forCount:),並將當前的 count 值作為引數傳入。

這裡使用了兩層可選鏈式呼叫。首先,由於 dataSource 可能為 nil,因此在 dataSource 後邊加上了 ?,以此表明只在 dataSource 非空時才去呼叫 increment(forCount:) 方法。其次,即使 dataSource 存在,也無法保證其是否實現了 increment(forCount:) 方法,因為這個方法是可選的。因此,increment(forCount:) 方法同樣使用可選鏈式呼叫進行呼叫,只有在該方法被實現的情況下才能呼叫它,所以在 increment(forCount:) 方法後邊也加上了 ?。

協議擴充套件

協議可以通過擴充套件來為遵循協議的型別提供屬性。方法以及下標的實現。通過這種方式,基於協議本身來實現這些功能,而無需在每個遵循協議的型別中重複同樣的實現,也無需使用全域性函式。

例如:通過擴充套件RandomNumberGenerator協議來提供randomBool()方法。該方法使用協議中定義的random方法來返回一個隨機bool值:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

通過協議擴充套件,所有遵循協議的型別,都能自動獲取這個擴充套件所增加的方法實現,無需任何額外修改:

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 列印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean:“(generator.randomBool())")
// 列印 “And here's a random Boolean: true

提供預設實現

可以通過協議擴充套件來為協議要求的屬性。方法以及下標提供預設的實現。如果遵循協議的型別為這些要求提供了自己的實現,那麼這些自定義實現將會替代擴充套件中的預設實現被使用。

通過協議擴充套件為協議要求提供的預設實現和可選的協議要求不同。雖然在這兩種情況下,遵循協議的型別都無需自己實現這些要求,但是通過擴充套件提供的預設實現可以直接呼叫,而無需使用可選鏈式呼叫。

為協議擴充套件新增限制條件

在擴充套件協議的時候,可以指定一些限制條件,只有遵循協議的型別滿足這些限制條件時,才能獲得協議擴充套件提供的預設實現。

這些縣紙條簡解除安裝協議名之後,使用where子句來描述。

extension CollectionType where Generator.Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" +itemsAsText.joinWithSeparator(", ") + "]"
    }
}

textualDescription 屬性返回整個集合的文字描述,它將集合中的每個元素的文字描述以逗號分隔的方式連線起來,包在一對方括號中。

textualDescription 屬性返回整個集合的文字描述,它將集合中的每個元素的文字描述以逗號分隔的方式連線起來,包在一對方括號中。

let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]

因為 Array 符合 CollectionType 協議,而陣列中的元素又符合 TextRepresentable 協議,所以陣列
可以使用 textualDescription 屬性得到陣列內容的文字表示:

可以使用 textualDescription 屬性得到陣列內容的文字表示:

注意
如果多個協議擴充套件都為同一個協議要求提供了預設實現,而遵循協議的型別又同時滿足這些協議擴充套件的限制條件,那麼將會使用限制條件最多的那個協議擴充套件提供的預設實現。

相關文章