Swift 中的協(xié)議(Protocol)是一種定義了方法、屬性和其他要求的藍(lán)圖。類、結(jié)構(gòu)體和枚舉可以遵循(Adopt)協(xié)議來提供這些要求的實(shí)現(xiàn)。通過協(xié)議,可以實(shí)現(xiàn)多種設(shè)計(jì)模式,例如面向接口編程和多重繼承。以下是關(guān)于 Swift 協(xié)議的概述。
定義協(xié)議
在 Swift 中,協(xié)議使用 protocol 關(guān)鍵字定義。協(xié)議可以規(guī)定類型必須實(shí)現(xiàn)的方法、屬性和操作符。
以下是一個(gè)簡(jiǎn)單的協(xié)議示例:
protocol Animal {
var name: String { get }
var sound: String { get }
func makeSound()
}
這個(gè)例子中,Animal 協(xié)議定義了兩個(gè)只讀屬性 name 和 sound,以及一個(gè)方法 makeSound。
遵循協(xié)議
類、結(jié)構(gòu)體和枚舉可以遵循協(xié)議以提供協(xié)議要求的實(shí)現(xiàn)。使用繼承實(shí)現(xiàn)多個(gè)協(xié)議時(shí),協(xié)議名稱用逗號(hào)分隔。遵循協(xié)議后,類型需要實(shí)現(xiàn)協(xié)議的所有要求。
struct Dog: Animal {
var name: String
var sound: String
func makeSound() {
print("\(name) makes sound: \(sound)")
}
}
let dog = Dog(name: "Buddy", sound: "Woof")
dog.makeSound() // 輸出: "Buddy makes sound: Woof"
在這個(gè)示例中,結(jié)構(gòu)體 Dog 遵循了 Animal 協(xié)議,并實(shí)現(xiàn)了協(xié)議定義的屬性和方法。
可選的實(shí)現(xiàn)
在某些情況下,協(xié)議可能需要定義可選的實(shí)現(xiàn)。這可以通過將協(xié)議與 @objc 屬性標(biāo)記并使用 optional 關(guān)鍵字實(shí)現(xiàn)。這種方式一般用于與 Objective-C 交互。
@objc protocol TestProtocol {
func requiredMethod()
@objc optional func optionalMethod()
}
這種情況下,遵循 TestProtocol 的類型只需要實(shí)現(xiàn) requiredMethod,而 optionalMethod 的實(shí)現(xiàn)是可選的。
協(xié)議屬性
協(xié)議可以定義實(shí)例屬性和類型屬性。協(xié)議屬性可以指定讀寫({ get set })或只讀({ get })要求。
protocol Vehicle {
var brand: String { get }
var numberOfWheels: Int { get set }
}
在這個(gè)示例中,Vehicle 協(xié)議定義了一個(gè)只讀屬性 brand 和一個(gè)可讀寫的屬性 numberOfWheels。
協(xié)議方法
協(xié)議可以定義實(shí)例方法和類型方法。定義協(xié)議方法時(shí),不需要提供方法內(nèi)部的實(shí)現(xiàn)。
protocol Playable {
func playSound()
static func description() -> String
}
在這個(gè)示例中,Playable 協(xié)議定義了一個(gè) playSound 實(shí)例方法和一個(gè) description 類型方法。遵循該協(xié)議的類、結(jié)構(gòu)體或枚舉需要實(shí)現(xiàn)這兩個(gè)方法。
mutating 方法
通常,“值類型”(例如結(jié)構(gòu)體和枚舉)在方法內(nèi)不能修改實(shí)例屬性。但是,在結(jié)構(gòu)體或枚舉內(nèi)的方法前加上 mutating 關(guān)鍵字,把這個(gè)方法標(biāo)記為可變方法(mutating method),可以讓方法修改實(shí)例屬性。
如果要讓協(xié)議方法修改實(shí)現(xiàn)類型的實(shí)例,可以使用 mutating 關(guān)鍵字標(biāo)記該方法。
在協(xié)議中使用 mutating 關(guān)鍵字的原因是讓協(xié)議方法具有靈活性。具體來說,在遵循協(xié)議的類型實(shí)現(xiàn)協(xié)議方法時(shí),在類中,即使方法沒有被標(biāo)記為 mutating,也可以修改實(shí)例屬性(因?yàn)轭愂且妙愋停?;而在結(jié)構(gòu)體和枚舉中,如果要修改實(shí)例屬性,則必須將方法標(biāo)記為 mutating。
我們通過一個(gè)示例來詳細(xì)了解 mutating 關(guān)鍵字在協(xié)議中的使用。下面是一個(gè)定義了一個(gè)名為 Switchable 的協(xié)議:
protocol Switchable {
var isOn: Bool { get set }
mutating func toggle()
}
這個(gè)協(xié)議要求遵循它的類型具有一個(gè) isOn 可讀寫屬性以及一個(gè) toggle 方法。我們?cè)?toggle 方法前加上了 mutating 關(guān)鍵字,允許方法修改遵循協(xié)議的類型的實(shí)例。
接下來,我們創(chuàng)建一個(gè)遵循 Switchable 協(xié)議的 LightSwitch 結(jié)構(gòu)體:
struct LightSwitch: Switchable {
var isOn: Bool = false
mutating func toggle() {
isOn.toggle()
}
}
在這個(gè)示例中,我們的 LightSwitch 結(jié)構(gòu)體遵循了 Switchable 協(xié)議,并實(shí)現(xiàn)了 isOn 和標(biāo)記為 mutating 的 toggle 方法。由于 toggle 方法被標(biāo)記為 mutating,它能夠修改結(jié)構(gòu)體內(nèi)的 isOn 屬性。
當(dāng)我們?cè)诖a中創(chuàng)建一個(gè) LightSwitch 實(shí)例并調(diào)用其 toggle 方法時(shí),實(shí)例的 isOn 屬性會(huì)發(fā)生改變:
var lightSwitch = LightSwitch()
print(lightSwitch.isOn) // 輸出:false
lightSwitch.toggle()
print(lightSwitch.isOn) // 輸出:true
初始化器要求
協(xié)議可以規(guī)定類型必須實(shí)現(xiàn)的指定初始化器。通過在協(xié)議中定義初始化器來實(shí)現(xiàn)這個(gè)要求。
protocol VehicleProtocol {
var brand: String { get }
init(brand: String)
}
在這個(gè)示例中,VehicleProtocol 協(xié)議定義了一個(gè)名為 brand 的只讀屬性和一個(gè)指定初始化器。遵循協(xié)議的類型需要實(shí)現(xiàn)這個(gè)初始化器以滿足協(xié)議要求。
類類型專屬的協(xié)議
你可以通過使用 AnyObject 關(guān)鍵字限制協(xié)議只能被類類型遵循,不能被結(jié)構(gòu)體和枚舉遵循。
protocol ClassOnlyProtocol: AnyObject {
func classOnlyMethod()
}
在這個(gè)示例中,ClassOnlyProtocol 只能被類類型遵循,這意味著結(jié)構(gòu)體和枚舉不能遵循這個(gè)協(xié)議。
協(xié)議繼承
Swift 中的協(xié)議可以繼承一個(gè)或多個(gè)其他協(xié)議。這允許創(chuàng)建更特定的要求來擴(kuò)展現(xiàn)有協(xié)議定義。
protocol Movable {
func move()
}
protocol Rotatable: Movable {
func rotate()
}
在這個(gè)示例中,Rotatable 協(xié)議繼承了 Movable 協(xié)議,這意味著遵循 Rotatable 協(xié)議的類型必須實(shí)現(xiàn) Movable 和 Rotatable 協(xié)議中的所有方法。
協(xié)議作為類型
協(xié)議可以作為函數(shù)參數(shù)、返回值、屬性類型以及數(shù)組或字典類型的組成元素。當(dāng)協(xié)議被用作類型時(shí),它表示所有遵循這個(gè)協(xié)議的類型。
protocol Printable {
func printDescription()
}
func printItems(_ items: [Printable]) {
for item in items {
item.printDescription()
}
}
struct Book: Printable {
var title: String
func printDescription() {
print("Book title: \(title)")
}
}
struct Movie: Printable {
var title: String
func printDescription() {
print("Movie title: \(title)")
}
}
let items: [Printable] = [Book(title: "The Catcher in the Rye"), Movie(title: "The Shawshank Redemption")]
printItems(items)
在這個(gè)示例中,我們定義了一個(gè)名為 Printable 的協(xié)議,它用于定義可以描述自己的類型,并將其用作函數(shù)參數(shù)。這使我們可以創(chuàng)建一個(gè)通用的 printItems 函數(shù),它接收一個(gè) Printable 類型的數(shù)組并打印每個(gè)元素的信息。
在 Swift 中,協(xié)議(Protocol)是一種定義了方法、屬性和其他功能要求的藍(lán)圖。類、結(jié)構(gòu)體和枚舉可以遵循(Adopt)協(xié)議來提供這些要求的實(shí)現(xiàn)。通過這種方式,Swift 可以實(shí)現(xiàn)多種設(shè)計(jì)模式,例如面向接口編程和多重繼承。
協(xié)議擴(kuò)展(Protocol Extension)
協(xié)議擴(kuò)展是一種在 Swift 中為協(xié)議提供默認(rèn)實(shí)現(xiàn)的方法。通過協(xié)議擴(kuò)展,可以快速為遵循一個(gè)或多個(gè)協(xié)議的類型提供默認(rèn)實(shí)現(xiàn)。這減少了代碼重復(fù),同時(shí)讓功能可以更方便地復(fù)用。
以下示例演示了協(xié)議擴(kuò)展的用法:
protocol Greeting {
func greet() -> String
}
// 為 Greeting 協(xié)議提供了一個(gè)默認(rèn)實(shí)現(xiàn)
extension Greeting {
func greet() -> String {
return "Hello, World!"
}
}
// 遵循 Greeting 協(xié)議
struct Person: Greeting {}
let person = Person()
print(person.greet()) // 輸出: "Hello, World!"
在這個(gè)示例中,Greeting 協(xié)議中定義了一個(gè) greet 方法,而協(xié)議擴(kuò)展為 Greeting 提供了默認(rèn)實(shí)現(xiàn)。當(dāng) Person 結(jié)構(gòu)體遵循 Greeting 協(xié)議時(shí),它不需要手動(dòng)實(shí)現(xiàn) greet 方法,而是可以直接使用協(xié)議擴(kuò)展提供的默認(rèn)實(shí)現(xiàn)。
協(xié)議組合(Protocol Composition)
協(xié)議組合是一種將多個(gè)協(xié)議組合到一個(gè)要求中的方法。這允許類型可以同時(shí)遵循多個(gè)協(xié)議。在 Swift 中,通過協(xié)議組合來實(shí)現(xiàn)多重繼承。
以下示例演示了協(xié)議組合的用法:
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 person = Person(name: "John Doe", age: 30)
wishHappyBirthday(to: person) // 輸出: "Happy birthday, John Doe, you're 30!"
在這個(gè)示例中,我們定義了兩個(gè)協(xié)議:Named 和 Aged。然后,我們創(chuàng)建了一個(gè)遵循這兩個(gè)協(xié)議的 Person 結(jié)構(gòu)體。通過協(xié)議組合,我們可以定義一個(gè)接受同時(shí)遵循 Named 和 Aged 協(xié)議的類型參數(shù)的 wishHappyBirthday 函數(shù)。這種方法實(shí)現(xiàn)了多重繼承,使得代碼更加靈活。
協(xié)議底層實(shí)現(xiàn)
要了解協(xié)議底層實(shí)現(xiàn)原理,我們需要深入探討 Swift 是如何在運(yùn)行時(shí)處理協(xié)議的。
在 Swift 中,協(xié)議通常借助一個(gè)叫做虛表(Protocol Witness Table,PWT)的結(jié)構(gòu)來實(shí)現(xiàn)。Swift 的編譯器為每個(gè)遵循協(xié)議的類型生成相應(yīng)的虛表,在運(yùn)行時(shí)通過表中的指針查找實(shí)現(xiàn)。下面我們分步詳細(xì)介紹這一過程:
協(xié)議定義:當(dāng)你定義一個(gè)協(xié)議,編譯器會(huì)為協(xié)議創(chuàng)建一個(gè)虛表原型,這個(gè)原型包含方法和屬性的簽名。虛表原型不包含任何具體的實(shí)現(xiàn),只是一種規(guī)范。
遵循協(xié)議:當(dāng)類型(如類、結(jié)構(gòu)體)遵循協(xié)議時(shí),編譯器會(huì)自動(dòng)生成一個(gè)虛表。這個(gè)虛表基于虛表原型,但包含類型對(duì)協(xié)議方法和屬性的具體實(shí)現(xiàn)。在運(yùn)行時(shí),Swift 使用表中的指針查找實(shí)現(xiàn)。
擴(kuò)展協(xié)議:若為協(xié)議提供擴(kuò)展以實(shí)現(xiàn)默認(rèn)方法,則在沒有提供自定義實(shí)現(xiàn)的情況下,虛表中的指針會(huì)指向這些默認(rèn)實(shí)現(xiàn)。
動(dòng)態(tài)調(diào)用:在運(yùn)行時(shí),程序需要調(diào)用遵循協(xié)議的實(shí)例相關(guān)的方法。此時(shí),Swift 會(huì)根據(jù)實(shí)例的類型查找對(duì)應(yīng)的虛表,然后通過虛表中方法的指針找到并執(zhí)行正確的實(shí)現(xiàn)。
這個(gè)底層實(shí)現(xiàn)模型使得協(xié)議遵循者能方便地使用動(dòng)態(tài)派發(fā)調(diào)用相應(yīng)的方法,保證了運(yùn)行時(shí)性能與安全。此外,這種模式也使得類型遵循協(xié)議時(shí)具有很高的靈活性,因?yàn)轭愋涂梢栽谶\(yùn)行時(shí)決定實(shí)現(xiàn)協(xié)議的具體方法和屬性。
雖然這里我們討論的是 Swift 協(xié)議遵循者的虛表實(shí)現(xiàn)原理,但我們也可以將這個(gè)模型應(yīng)用于其他 Swift 內(nèi)部結(jié)構(gòu),例如:類繼承。值得注意的是, Swift 的協(xié)議底層實(shí)現(xiàn)可能會(huì)在未來版本中繼續(xù)改進(jìn)和優(yōu)化。