Swift 4.0 編程語言(七)

132.轉換錯誤成可選值

通過轉換錯誤成一個可選值,你可以使用 try? 來處理錯誤。當執行try?表達式時,如果一個錯誤拋出來了,表達式的值就會變成nil. 例如, 下面的例子里x和y有著相同的值和行為:

func someThrowingFunction() throws -> Int {

// ...

}let x = try? someThrowingFunction()

let y: Int?

do {

y = try someThrowingFunction()

} catch {

y = nil

}

如果 someThrowingFunction() 拋出一個錯誤, x和y的值就是nil. 否則, x和y值就是函數的返回值。注意,無論函數返回什么類型,x和y都是可選的。這里函數返回的是一個整數, 所以x和y是可選的整數。

當你想處理所有相同方式的錯誤時,使用 try? 讓你可以寫出更加精確的錯誤處理代碼。例如, 下面的代碼使用了一些簡單的方法來獲取數據, 如果所有的方法都失敗了則返回nil.

func fetchData() -> Data? {

if let data = try? fetchDataFromDisk() { return data }

if let data = try? fetchDataFromServer() { return data }

return nil

}

禁用錯誤傳遞

有時,你不希望一個拋出函數或者方法在運行時拋出一個錯誤。這種情況下, 可以在表達式前面寫上 try!來禁用錯誤傳遞并且把調用包裹在運行的斷言中,這錯誤就不會拋出。如果一個錯誤時間上出現了, 你會得到一個運行時錯誤。

例如, 下面的代碼使用了 loadImage(atPath:) 函數, 它從一個給定的路徑導入圖片資源,如果圖片不能導入則拋出一個錯誤。在這種情況下, 因為圖片是靠程序輸送, 運行時不會有錯誤拋出, 所以它適合禁用錯誤傳遞。

let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

指定清理行為

在代碼執行離開當前代碼塊之前,你可以用一個defer語句來執行一組語句。這個語句讓你可以做一些必須的清理工作,這個工作執行時無視怎樣離開當前代碼塊—不管它是因為錯誤發生還是因為return或者break語句。例如, 你可以使用一個defer語句,來確保文件描述符被關閉,手動分配的內存被釋放。

一個defer語句一直到當前代碼塊退出才會執行。這個語句由defer關鍵字和執行語句組成。推遲語句不會包含任何把控制轉移的語句。 比如 break 或者 return 語句, 也不會拋出錯誤。推遲行為以它們被指定的相反順序執行—也就說, defer語句第一行代碼會在第二行代碼后執行,以此類推。

func processFile(filename: String) throws {

if exists(filename) {

let file = open(filename)

defer {

close(file)

?}

while let line = try file.readline() {

// Work with the file.

?}

// close(file) is called here, at the end of the scope.

?}

}

上面的例子使用了一個 defer 語句來確保 open(:) 函數會調用 close(:).

備注:即使沒有涉及錯誤處理代碼,你也可以使用一個defer語句。

類型轉換

類型轉換是是判斷實例類型的一種方法, 或者在它的繼承層次里,把來自某些地方的實例作為不同的超類或者子類。

Swift中的類型轉換使用is和as操作符。這兩個操作符提供了簡單有效的方式來判斷一個值的類型或者把一個值轉換成不同的類型。

你可以通過類型轉換來判斷一個類型是否遵守某個協議, hecking for Protocol Conformance 有描述。

為類型轉換定義類層次

你可以在類和子類層次使用類型轉換,來判斷特定類實例的類型,同時可以在相同的層次里把實例轉換成另外一個類類型。下面三個代碼片段定義了一類層次,還有一個包含這些類實例的數組, 在類型轉換的例子中使用。

第一個代碼片段定義了了一個新的基類 MediaItem. 這個類為任何出現在數字媒體庫中的項目提供基本的功能。特別的是, 它聲明了一個字符串類型的 name屬性, 還有個一個 init 構造器。 (它假定所有媒體項,包括電影和歌曲,都有名字)

class MediaItem {

var name: String

init(name: String) {

self.name = name

? ?}

}

下面的代碼片段定義了兩個 MediaItem的子類。第一個子類, Movie, 封裝了一部電影的額外信息。它添加了一個 director 屬性和一個對應的構造器。第二個子類, Song, 添加了一個add屬性和一個構造器:

class Movie: MediaItem {

var director: String

init(name: String, director: String) {

self.director = director

super.init(name: name)

? ? ?}

}

class Song: MediaItem {

var artist: String

init(name: String, artist: String) {

self.artist = artist

super.init(name: name)

? ? } ?

}

最后的代碼片段創建了一個常量數組 library, 它包括了兩個 Movie 實例和三個 Song 實例。library 數組的類型通過用字面量內容初始化可以推斷出來。 Swift 類型檢查者可以推斷出 Movie 和 Song 有一個共同的基類 MediaItem, 所以它推斷library 數組是 [MediaItem] 類型:

let library = [

Movie(name: "Casablanca", director: "Michael Curtiz"),

Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),

Movie(name: "Citizen Kane", director: "Orson Welles"),

Song(name: "The One And Only", artist: "Chesney Hawkes"),

Song(name: "Never Gonna Give You Up", artist: "Rick Astley")

]

// the type of "library" is inferred to be [MediaItem]

實際上存儲在 library 數組里的依然是 Movie 和 Song 實例。不過, 如果遍歷這個數組的內容, 你獲得的類型是 MediaItem, 而不是 Movie 或者 Song. 為了使用它們本來的類型, 你需要檢測它們的類型, 或者向下轉換它們的類型。0

判斷類型

使用類型判斷操作符 (is) 來判斷一個實例是否是每個子類類型。如果實例是那個子類類型,類型判斷操作符就返回真,否則就返回假。

下面的例子定義了兩個變量, movieCount 和 songCount, 用來計算存儲在library數組中的 Movie 和 Song 實例的數量:

var movieCount = 0

var songCount = 0

for item in library {

if item is Movie {

movieCount += 1

? ?} else if item is Song {

songCount += 1

? ?}

}

print("Media library contains \(movieCount) movies and \(songCount) songs")

// 打印 "Media library contains 2 movies and 3 songs"

上面的例子遍歷library數組所有的項。每次傳遞, for-in 循環把item常量設置成數組里的下一個 MediaItem.

如果當前MediaItem 是一個Movie 實例, 就返回真。否則就返回假。類似的, MediaItem是 Song 就判斷它是不是一個 Song 實例。for-in 循環結束后, movieCount 和 songCount 包含了對應實例的數量。

133.向下轉換

實際上,特定類類型的常量或者變量調用是子類的實例。你可以使用類型轉換操作符(as? 或者 as!)向下轉換成子類類型。

由于向下轉換會失敗, 所以類型轉換操作符有兩種形式。條件形式, as?, 返回你想轉換類型的可選值。強制形式, as!, 嘗試向下轉換并強制拆包結果。

當你不能確定向下轉換能否成功時,使用條件形式的類型轉換操作符 (as?). 這個操作符總是返回一個可選值, 如果向下轉換不可能就會返回nil. 這個確保你可以判斷轉換的成功。

當你確定向下轉換可以成功時,使用強制形式的類型轉換操作符 (as!). 如果你轉換到一個錯誤的類類型,這個形式的操作符會引發一個運行時的錯誤。

下面的例子遍歷了library數組里的的每一個 MediaItem , 然后為每一項打印一條對應的描述信息。為了實現這個, 它需要每一項作為真實的 Movie 或者 Song來訪問, 不僅僅是作為 MediaItem. 為了能可以訪問 director 或者 artist 屬性,然后用在描述里,這樣做是必須的。

在這個例子里, 數組里的每一項有可能是 Movie, 或者 Song. 你不會提前知道每一項實際使用哪個類, 所以這里很適合使用條件形式的操作符 (as?) 來在循環中判斷向下轉換成功與否:

for item in library {

if let movie = item as? Movie {

print("Movie: \(movie.name), dir. \(movie.director)")

? ? } else if let song = item as? Song {

print("Song: \(song.name), by \(song.artist)")

? }

}

// Movie: Casablanca, dir. Michael Curtiz

// Song: Blue Suede Shoes, by Elvis Presley

// Movie: Citizen Kane, dir. Orson Welles

// Song: The One And Only, by Chesney Hawkes

// Song: Never Gonna Give You Up, by Rick Astley

這個例子開始先嘗試將當前項向下轉換到類型 Movie. 因為當前項是 MediaItem 實例, 它有可能是一個 Movie; 它也有可能是一個 Song, 或者僅僅是一個基本的 MediaItem. 因為不確定性, as? 形式的類型轉換符會返回一個可選值,在嘗試向下轉換成一個子類型時。item as? Movie 返回結果是 Movie? 類型, 或者 “可選的 Movie”.

如果對數組中的Song實例向下轉換到 Movie 就會失敗。為了處理這個情況, 例子里使用可選綁定來判斷可選的l Movie 是否包含了一個值 (就是說, 判斷向下轉換是否成功) 可選綁定寫作 “if let movie = item as? Movie”, 它可以這樣讀:

“嘗試作為一個Movie來訪問。 如果成功了, 把可選返回值設置給一個臨時常量 movie.”

如果向下轉換成功了, movie的屬性用來為Movie實例打印一條描述信息, 包括導演名。相似的原則用于判斷 Song 實例, 在找到Song實例的時候就打印對應的描述信息(包括藝術家名字)。

備注:轉換不會修改實例的值。潛在的實例依然相同; 它會簡單處理和訪問作為轉換的類型。

134.Any 和 AnyObject的類型轉換

Swift 提供兩個特別類型來使用未指定的類型:

Any 可以表示任意類型的實例, 包括函數類型。

AnyObject可以表示任意類類型的實例。

當你需要它們提供的行為和能力的時候, 可以使用Any 和 AnyObject. 更好的建議是,在你的代碼中指定你希望的類型。

這里有一個例子使用Any來處理不同類型的混合, 包括函數類型和非類的類型。這個例子創建了一個數組 things, 它可以存儲Any類型的值:

var things = [Any]()

things.append(0)

things.append(0.0)

things.append(42)

things.append(3.14159)

things.append("hello")

things.append((3.0, 5.0))

things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))

things.append({ (name: String) -> String in "Hello, \(name)" })

things 數組包含了兩個整型值, 兩個浮點值, 一個字符串值, 一個元組類型 (Double, Double), 電影 “Ghostbusters”, 和一組表達式,這個表達式有一個字符串參數和一個字符串返回值。

為了找出一個常量或者變量的特定類型, Any 和 AnyObject 類型, 你可以在一個switch語句分支里使用is或者as. 下面的例子遍歷了things數組里的每一項,然后使用一個switch語句來查詢它們的類型。一些分支綁定了指定類型常量的匹配值來確保它的值被打印出來:

for thing in things {

switch thing {

case 0 as Int:

print("zero as an Int")

case 0 as Double:

print("zero as a Double")

case let someInt as Int:

print("an integer value of \(someInt)")

case let someDouble as Double where someDouble > 0:

print("a positive double value of \(someDouble)")

case is Double:

print("some other double value that I don't want to print")

case let someString as String:

print("a string value of \"\(someString)\"")

case let (x, y) as (Double, Double):

print("an (x, y) point at \(x), \(y)")

case let movie as Movie:

print("a movie called \(movie.name), dir. \(movie.director)")

case let stringConverter as (String) -> String:

print(stringConverter("Michael"))

default:

print("something else")

? ? }

}

// zero as an Int

// zero as a Double

// an integer value of 42

// a positive double value of 3.14159

// a string value of "hello"

// an (x, y) point at 3.0, 5.0

// a movie called Ghostbusters, dir. Ivan Reitman

// Hello, Michael

備注:Any 類型表示任意類型值, 包括可選類型。如果某個地方需要一個Any類型值,你而你使用了可選值, Swift 會給你一個警告。如果你真的需要使用一個可選值作為 Any 值, 你可以使用 as 操作符顯式把可選項轉換為 Any, 就像下面展示的這樣。

let optionalNumber: Int? = 3

things.append(optionalNumber)? ? ? ? // Warning

things.append(optionalNumber as Any) // No warning

嵌套類型

枚舉通常用來支持一個特定類或者結構體的功能。類似的, 有時候在一個復雜類型的內部定義工具類或者結構體來使用是很方便的。為了完成這個, Swift 確保你可以定義嵌套類型, 即在支持類型定義中內嵌支持的枚舉,類和結構體。

為了在其他類型中內嵌一個類型, 在它支持類型的外部大括號內寫定義。類型可以按照需要內嵌多層。

內嵌類型的行為

下面的例子定義了一個結構體 BlackjackCard, 它模擬了在Blackjack 游戲中玩牌。BlackJack 結構體包含了兩個內嵌的枚舉類型 Suit 和 Rank.

在 Blackjack里, Ace 牌面值或者是一或者是十一。這個特性用一個結構體 Values來表示, 它嵌套在 Rank 枚舉里:

struct BlackjackCard {

// nested Suit enumeration

enum Suit: Character {

case spades = "?", hearts = "?", diamonds = "?", clubs = "?"

? }

// nested Rank enumeration

enum Rank: Int {

case two = 2, three, four, five, six, seven, eight, nine, ten

case jack, queen, king, ace

struct Values {

let first: Int, second: Int?

?}

var values: Values {

switch self {

case .ace:

return Values(first: 1, second: 11)

case .jack, .queen, .king:

return Values(first: 10, second: nil)

default:

return Values(first: self.rawValue, second: nil)

? ?}

? }

}

// BlackjackCard properties and methods

let rank: Rank, suit: Suit

var description: String {

var output = "suit is \(suit.rawValue),"

output += " value is \(rank.values.first)"

if let second = rank.values.second {

output += " or \(second)"

? }

return output

?}

}

Suit 枚舉描述了玩牌的四種花色, 帶有原始字符值來表示它們的符號。

Rank 枚舉描述了13種玩牌排名, 帶有一個原始整型值來表示它們的牌面值 (這個原始整型值沒有用于Jack, Queen, King, 和 Ace)

正如上面提到的,Rank 枚舉定義了更深的嵌套結構體 Values. 這個結構體封裝了大多數牌都有一個值的事實, 不過 Ace 牌有兩個值。 Values 結構體定義了兩個屬性來表示這個:

第一個, 類型是 Int

第二個, 類型是 Int?, 或者“可選 Int”

Rank 同時定義了一個計算屬性values, 它會返回 Values 結構體的實例。這個計算屬性考察牌的排名并且基于排名用對應的值來初始化新的 Values 實例。它為jack, queen, king, 和 ace使用特別的值。對于數子牌, 它使用原始整型值。

BlackjackCard 結構體自己有兩個屬性—rank 和 suit. 它也定義了一個計算屬性description, 它使用rank 和 suit中的值建立牌名字和數值的描述。description 屬性使用可選綁定來判斷是否有第二個值需要展示, 如果是, 把第二個值插入描述信息。

由于 BlackjackCard 是一個沒有自定義構造器的結構體, 它有一個隱式的成員構造器。你可以用這個構造器來構造一個新的常量theAceOfSpades:

let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)

print("theAceOfSpades: \(theAceOfSpades.description)")

// 打印 "theAceOfSpades: suit is ?, value is 1 or 11"

盡管 Rank 和 Suit 嵌套在 BlackjackCard里面, 它們的類型可以通過上下文推斷出來, 所以這個實例的初始化可以根據分支名調用枚舉分支 (.ace 和 .spades) . 上面的例子里, description 屬性正確報出黑桃 Ace 牌面值是1或者11.

調用嵌套類型

在定義上下文之外調用嵌套類型, 要在它的名字前加上它內嵌的類型名:

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue

// heartsSymbol is "?"

擴展

擴展可以給存在的類,結構體,枚舉或者協議類型添加新功能。包括給你無法訪問的源代碼擴展類型的能力 (也就是逆向建模)。 擴展類似于Objective-C 的分類。 (跟 Objective-C 分類的是, Swift 擴展沒有名字)

Swift 擴展可以:

添加計算實例屬性和計算類型屬性

定義實例方法和類型方法

提供新的構造器

定義下標

定義和使用新的嵌套類型

讓已存在的類型符合一個協議

在 Swift里, 你甚至可以擴展一個協議來提供需要的實現或者或者添加額外的功能符合使用的類型。

備注:擴展可以給一個類型添加新功能, 但是它們不能覆蓋已經存在的功能。

擴展語法

使用 extension 關鍵字來聲明擴展:

extension SomeType {

// new functionality to add to SomeType goes here

}

擴展可以讓已存在的類型符合一個或者多個協議。 在這種情況下, 協議名跟類和結構體的名字寫法一樣:

extension SomeType: SomeProtocol, AnotherProtocol {

// implementation of protocol requirements goes here

}

備注:如果定義一個擴展來給已存在的類型添加新功能, 這個新功能給所有這種類型的實例使用, 即使它們在擴展定義前已經創建。

計算屬性

擴展可以給已存在類型添加計算實例屬性和計算類型屬性。下面的例子給Swift的內建浮點類型添加了五個計算實例屬性, 提供了基本支持來使用距離單位:

extension Double {

var km: Double { return self * 1_000.0 }

var m: Double { return self }

var cm: Double { return self / 100.0 }

var mm: Double { return self / 1_000.0 }

var ft: Double { return self / 3.28084 }

}

let oneInch = 25.4.mm

print("One inch is \(oneInch) meters")

// 打印 "One inch is 0.0254 meters"

let threeFeet = 3.ft

print("Three feet is \(threeFeet) meters")

// 打印 "Three feet is 0.914399970739201 meters"

這些計算屬性表示,一個浮點值應該被當做一個特定的長度單位。盡管它們實現成計算屬性, 這些屬性的名字可以用點語法添加到浮點值的后面, 使用這些字面量來執行距離的轉換。

在這個例子里, 浮點值 1.0 被認為表示1米。就是為什么m計算屬性返回self—1.m 表達式被認為是計算1.0的浮點值。

其他單位轉換使用米測量。一千米相當于 1,000 米, 所以km計算屬性把數值乘以1_000.00 來轉換成用米表示的數。相似的, 一米有3.28084 尺, 所以 ft 計算屬性用潛在的浮點數值除以3.28084, 把尺轉換為米。

這些屬性是只讀的計算屬性, 所以它們表示的時候沒有帶上 get 關鍵字, 為了簡潔性。它們的返回值是浮點型, 可以用在接受浮點數的數學運算中。

let aMarathon = 42.km + 195.m

print("A marathon is \(aMarathon) meters long")

// 打印 "A marathon is 42195.0 meters long"

構造器

擴展可以給已存在類型添加新的構造器。這讓你可以擴展其他類型來接受你自定的類型作為構造器的參數, 或者提供額外的構造選項,這些不會包含在類型的原始實現代碼中。

擴展可以給一個類添加新的便利構造器, 但是不能添加新的指定構造器或者析構器。指定構造器和析構器必須在原始類實現中提供。

備注:如果使用擴展為值類型添加一個構造器, 為所有的存儲屬性提供默認值,同時不會定義任何自定義的構造器。你可以在你的擴展構造器里調用默認的構造器和成員構造器。

下面的例子定義了一個 Rect 結構體來表示一個算術矩形。這個例子同時定義了兩個輔助的結構體Size 和 Point, 兩個結構體都給所有的屬性提供默認值 0.0:

struct Size {

var width = 0.0, height = 0.0

}

struct Point {

var x = 0.0, y = 0.0

}

struct Rect {

var origin = Point()

var size = Size()

}

由于 Rect 結構體給所有的屬性提供了默認值, 它會有一個默認的構造器和一個成員構造器。 這些構造器可以用來創建新的 Rect 實例:

let defaultRect = Rect()

let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),

size: Size(width: 5.0, height: 5.0))

你可以擴展Rect結構體,提供一個額外的構造器,帶有一個指定的中心點和大小:

extension Rect {

init(center: Point, size: Size) {

let originX = center.x - (size.width / 2)

let originY = center.y - (size.height / 2)

self.init(origin: Point(x: originX, y: originY), size: size)

? ?}

}

新的構造器一開始先基于提供的中心點和大小,計算出一個對應的原點。然后調用結構體的成員構造器 init(origin:size:), 它在對應的屬性中存儲最新的原點和大小值:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),

size: Size(width: 3.0, height: 3.0))

// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

備注:如果你用擴展來提供新的構造器, 你有責任確保初始化完成時,所有實例都被完全的構造了。

方法

擴展可以給已存在的類型添加實例方法和類型方法。下面的例子給Int類型添加了一個新的實例方法repetitions:

extension Int {

func repetitions(task: () -> Void) {

for _ in 0..

repetitions(task:) 方法只有一個參數 () -> Void, 它表明一個沒有參數和返回值的函數。

擴展定義之后, 你可以在任何整數上調用 repetitions(task:) 方法來執行多次同樣的任務:

3.repetitions {

print("Hello!")

}

// Hello!

// Hello!

// Hello!

改變實例方法

用擴展添加的實例方法也可以改變實例本身。結構體和枚舉方法,能改變自身或者屬性的必須標記為 mutating, 就像來自原始實現的 mutating 方法一樣。

下面的例子給Swift的整型添加了一個新的 mutating 方法, 它用來計算原始值的平方:

extension Int {

mutating func square() {

self = self * self

? ?}

}

var someInt = 3

someInt.square()

// someInt is now 9

下標

擴展可以給已存在的類型添加下標。下面的例子給Swift 內建整型添加了個整型下標。下標 [n] 返回從右邊算第n個十進制數字:

123456789[0] returns 9

123456789[1] returns 8

…and so on:

extension Int {

subscript(digitIndex: Int) -> Int {

var decimalBase = 1

for _ in 0..

如果整數沒有足夠的位數滿足請求索引, 下標實現會返回 0, 好像這個數字左邊填充了0:

746381295[9]

// returns 0, as if you had requested:

0746381295[9]

嵌套類型

擴展可以給已存在的類,結構體和枚舉添加新的嵌套類型:

extension Int {

enum Kind {

case negative, zero, positive

?}

var kind: Kind {

switch self {

case 0:

return .zero

case let x where x > 0:

return .positive

default:

return .negative

?}

?}

}

這個例子向Int類型添加了一個新的嵌套枚舉。這個枚舉 Kind, 表示一個整數表示的數字種類。特別的是, 它表示這個數組是否是正數,0,或者負數。

這個例子同時也添加了一個新的計算屬性 kind, 它會根據整數返回對應的 Kind 枚舉分支。

這個嵌套的枚舉可以用在任意整數值:

func printIntegerKinds(_ numbers: [Int]) {

for number in numbers {

switch number.kind {

case .negative:

print("- ", terminator: "")

case .zero:

print("0 ", terminator: "")

case .positive:

print("+ ", terminator: "")

? }

?}

print("")

}

printIntegerKinds([3, 19, -27, 0, -6, 0, 7])

// 打印 "+ + - 0 - 0 + "

這個函數 printIntegerKinds(_:), 輸入一個整數數組然后遍歷這些值。對于數組中的每一個整數來說, 函數會檢查它的kind計算屬性, 然后打印對應的描述信息。

備注:number.kind 已經知道是 Int.Kind 類型。因為這個原因, 所有 Int.Kind 分支值在switch語句中都是簡寫, 比如 .negative rather 而不是 Int.Kind.negative.

135.協議

協議定義了方法, 屬性和其他要求的藍圖。它適合特定的任務或者部分功能。協議可以被類, 結構體或者枚舉采用, 實現實際的需求。任何滿足這個協議要求的類型都可以說是符合了這個協議。

除了指定需求必須實現, 你可以擴展一個協議來實現其他的需求或者實現額外的功能。

協議語法

定義協議的方式跟類,結構體和枚舉很相似:

protocol SomeProtocol {

// protocol definition goes here

}

自定義類型規定,遵守某個協議,要把協議名寫在類型名的后面,用冒號分開。多個協議可以用逗號隔開:

struct SomeStructure: FirstProtocol, AnotherProtocol {

// structure definition goes here

}

如果一個類有超類, 把超類名寫在所有協議的前面, 后面跟著逗號:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {

// class definition goes here

}

屬性需求

一個協議可以要求符合類型去提供一個帶有名字和類型的實例屬性或者類型屬性。協議不能指定屬性是否應該是一個存儲屬性還是一個計算屬性—它只能指定需要的屬性的名字和類型。協議也指定了是否每一個屬性必須是 gettable 或者同時是 gettable 和 settable.

如果一個協議要求屬性是 gettable 和 settable, 這個屬性要求對于一個常量存儲屬性或者一個只讀計算屬性是沒有辦法完成的。如果協議只要求屬性是 gettable, 這個要求可以被任意類型的屬性滿足, 如果這個對你自己的代碼有用的話,對于settable的屬性也是有效的。

屬性需求總是聲明為變量屬性, 前面是var關鍵字。Gettable 和 settable 屬性,在它們的類型聲明后面寫上 { get set } 來標明。gettable 屬性只需寫上 { get }.

protocol SomeProtocol {

var mustBeSettable: Int { get set }

var doesNotNeedToBeSettable: Int { get }

}

當你定義類型屬性需求時,總是要在它們前面加上static 關鍵字。盡管類型屬性需求前綴可以是static或者class,這個規則在用類實現的時候同樣適用:

protocol AnotherProtocol {

static var someTypeProperty: Int { get set }

}

這里有一個例子,協議只有一個實例屬性需求:

protocol FullyNamed {

var fullName: String { get }

}

FullyNamed 協議要求符合類型提供一個完全限定的名字。協議不能指定其他關于符合類型本身的東西—它只能指定類型必須提供一個全名。協議規定任何 FullyNamed 類型必須有一個 gettable 實例屬性 fullName, 它是一個字符串類型。

這里有一個結構體的例子,它采用并符合了 FullyNamed 協議:

struct Person: FullyNamed {

var fullName: String

}

let john = Person(fullName: "John Appleseed")

// john.fullName is "John Appleseed"

這個例子定義了一個結構體 Person, 它表示一個指定名字的人。它規定,采用 FullyNamed 協議作為實現的第一行的部分。

Person 每個實例都只有一個存儲屬性 fullName, 它們是字符串類型。這個匹配了 FullyNamed 協議的唯一需求, 說明 Person 正確遵守了這個協議。 (如果協議要求沒有實現,Swift 會報編譯期錯誤)

這里有個更復雜的類, 它也是采用并遵守了 FullyNamed 協議:

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

}

}

var ncc1701 = Starship(name: "Enterprise", prefix: "USS")

// ncc1701.fullName is "USS Enterprise"

這個類把fullName 屬性實現為一個只讀計算屬性。 每個Starship 類實例存儲一個強制名和一個可選的前綴。如果前綴存在,fullName 屬性就使用它。放在 name 前,為 starship 創建一個完整的名字。

方法需求

協議可以要求符合類型實現特定的實例方法和類型方法。這些方法寫在協議定義里面,跟普通實例方法和類型方法寫法一致。但是沒有花括號或者方法體。允許有可變參數, 遵守普通方法的規則限制。不過,協議定義中的方法參數不能指定默認值。

像類型屬性需求一樣, 定義在協議里的類型方法需要在前面加上static關鍵字:

protocol SomeProtocol {

static func someTypeMethod()

}

下面的例子,定義了一個協議,里面有一個實例方法:

protocol RandomNumberGenerator {

func random() -> Double

}

這個協議, RandomNumberGenerator, 要求任何符合類型必須有一個實例方法 random, 這個方法調用后會返回一個浮點值。 盡管它不是協議指定的部分, 它假設這個值會從0.0 到 1.0 (但是不包括)

RandomNumberGenerator 協議不會假設每個隨機數怎么產生—它簡單要求生成器提供一個標準方法來產生新的隨機數。

這里實現了一個類,它采用并遵守了 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).truncatingRemainder(dividingBy: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"

變異方法需求

對于方法來說,有時候需要改變或者編譯它所屬的實例。對于值類型的實例方法 (結構體和枚舉),可以在func關鍵字前加上 mutating 關鍵字來表明,允許這個方法改變它所屬的實例和這個實例的所有屬性。

如果你定義了一個協議實例方法,想要改變采用這個協議的任意類型的實例, 在協議定義里,在方法前標記上 mutating 關鍵字。 這讓結構體和枚舉可以采用這個協議并且滿足方法需求。

備注:如果你把協議實例方法需求標記為 mutating, 那么在為類實現該方法的時候,就不需要寫上 mutating 關鍵字。mutating 關鍵字只是用在結構體和枚舉上。

下面的例子定義了一個協議 Togglable, 它只定義了一個實例方法需求 toggle. 就像它名字提示的, toggle() 方法會觸發或者改變符合類型的狀態。通常是修改類型的某個屬性。

toggle() 方法用 mutating 關鍵字標記,作為Togglable 協議定義的一部分。表示被調用時,希望改變符合類型實例的狀態:

protocol Togglable {

mutating func toggle()

}

如果你為一個結構體或者枚舉實現這個協議, 通過實現標記為mutating 的toggle()方法,這個結構體或者枚舉就可以符合這個協議了。

下面的例子定義了一個枚舉 OnOffSwitch. 這個枚舉開關有兩個狀態, 用枚舉分支 on 和 off表示。枚舉開關實現標記為 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 is now equal to .on

構造器需求

協議可以要求符合類型實現指定的構造器。協議定義中的構造器和普通構造器的寫法是一樣的, 但是沒有花括號和構造體:

protocol SomeProtocol {

init(someParameter: Int)

}

類實現協議構造器需求

你可以在符合協議的類中,把協議構造器實現成指定構造器或者便利構造器。在這兩種情況下, 你必須用required 修飾符來標記構造器的實現:

class SomeClass: SomeProtocol {

required init(someParameter: Int) {

// initializer implementation goes here

? ?}

}

使用required 修飾符確保你在所有符合協議的子類上,提供一個顯式或者繼承的構造器需求實現, 它們也遵守這個協議。

備注:如果符合協議的類標記了final修飾符,你就不需要用required標記協議構造器的實現,因為final 類不能子類化。

如果一個子類重寫了超類的指定構造器, 然也實現了協議的構造器需求, 這個構造器就要同時標記required 和 override的修飾符:

protocol SomeProtocol {

init()

}

class SomeSuperClass {

init() {

// initializer implementation goes here

}

}

class SomeSubClass: SomeSuperClass, SomeProtocol {

// "required" from SomeProtocol conformance; "override" from SomeSuperClass

required override init() {

// initializer implementation goes here

}

}

可失敗構造器需求

協議可以為符合類型定義失敗構造器。

符合類型的失敗或者非失敗構造器可以滿足協議的失敗構造器需求。而一個非失敗構造器需求,只能用一個非失敗構造器或者一個隱式拆包失敗構造器才能滿足。

協議作為類型

協議本身不實現任何功能。 盡管如此, 你創建的協議在代碼中依然是完全成熟的類型。

因為協議是一個類型, 只要其他類型允許,你可以在很多地方使用它。包括:

在函數,方法或者構造器中作為參數類型或者返回類型

作為一個常量,變量或者屬性類型

作為數組,字典或者其他容器中項目的類型

備注 :因為協議是類型, 它們的名字開始是一個大寫字母 (例如 FullyNamed 和 RandomNumberGenerator) ,和Swift 中其他類型名字很匹配 (例如 Int, String, 和 Double).

這里有一個協議作為類型使用的例子:

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協議的類型來設置它。 你賦值給這個屬性的實例無需其他, 主要這個實例采用RandomNumberGenerator 協議即可。

Dice 也有一個構造器, 來設置它的初始狀態。這個構造器有一個參數 generator, 它也是 RandomNumberGenerator 類型。在初始化新的Dice實例時,你可以傳入任何符合類型的值作為這個參數。

Dice 還提供了一個實例方法, roll, 它返回骰子的值。這個方法調用了生成器的 random() 方法來創建一個0.0到1.0的隨機數。 然后使用隨機數來生成一個正確的值。因為生成器采用了 RandomNumberGenerator, 它會保證有一個random() 方法可以調用。

這里展示Dice類如何產生一個6面的骰子, 它使用一個LinearCongruentialGenerator 實例作為隨機數生成器:

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

委托

委托是一種設計模式, 讓類或者結構體可以把自己的一些責任委托給其他類型的實例。這個設計模式通過定義協議來實現,這個協議封裝了委托的責任。符合類型確保提供委托的功能。委托可以用來響應特定的行為, 或者無需知道潛在資源類型,即可從資源獲取數據。

下面的例子定義了兩個協議, 用來使用基于骰子的棋盤游戲:

protocol DiceGame {

var dice: Dice { get }

func play()

? }

protocol DiceGameDelegate {

func gameDidStart(_ game: DiceGame)

func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)

func gameDidEnd(_ game: DiceGame)

}

DiceGame 協議可以被任何包含骰子的游戲采用。DiceGameDelegate 協議可以被任何跟蹤DiceGame過程的類型采用。

這里是有一個前面控制流中介紹過的蛇與梯子的游戲。這個版本使用一個 Dice 實例來搖骰子; 采用了 DiceGame 協議; 然后通知DiceGameDelegate 游戲的進程:

class SnakesAndLadders: DiceGame {

let finalSquare = 25

let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())

var square = 0

var board: [Int]

init() {

board = Array(repeating: 0, count: finalSquare + 1)

board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02

board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08

? ?}

var delegate: DiceGameDelegate?

func play() {

square = 0

delegate?.gameDidStart(self)

gameLoop: while square != finalSquare {

let diceRoll = dice.roll()

delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)

switch square + diceRoll {

case finalSquare:

break gameLoop

case let newSquare where newSquare > finalSquare:

continue gameLoop

default:

square += diceRoll

square += board[square]

? ? ? ?}

? ? }?

delegate?.gameDidEnd(self)

? }?

}

這個版本的游戲包裝成一個類 SnakesAndLadders, 它采用了 DiceGame 協議。它提供了一個 gettable dice 屬性和一個 play() 方法來遵守這個協議。(dice 屬性聲明為常量,因為它在初始化后就不需要變化, 協議只要求它是 gettable.)

蛇與梯子游戲的棋盤在構造器 init() 里設置。游戲的所有邏輯都搬到協議的play 方法里, 它使用協議的 required dice 屬性來提供骰子值。

注意 delegate 屬性定義為可選的 DiceGameDelegate, 因為委托不要求必須玩游戲。因為它是一個可選類型, delegate屬性自動初始化為nil. 之后, 這個游戲實例會有一個選項來設置這個屬性為一個固定的代理。

DiceGameDelegate 提供了三個方法來跟蹤游戲的過程。 這三個方法已經合并進 play()方法中。在新游戲開始時會調用, 新一輪開始, 或者游戲結束。

由于delegate 屬性是一個可選的DiceGameDelegate, play()在代理上調用一個方法時, 它會使用可選鏈接。如果 delegate 屬性為nil, 這些調用會失敗而不會發生錯誤。如果 delegate 屬性非nil, 代理方法會被調用, 然后傳入SnakesAndLadders 實例作為一個參數。

下面例子展示了一個類 DiceGameTracker, 它采用了 DiceGameDelegate 協議:

class DiceGameTracker: DiceGameDelegate {

var numberOfTurns = 0

func gameDidStart(_ game: DiceGame) {

numberOfTurns = 0

if game is SnakesAndLadders {

print("Started a new game of Snakes and Ladders")

? ?}

print("The game is using a \(game.dice.sides)-sided dice")

? ?}

func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {

numberOfTurns += 1

print("Rolled a \(diceRoll)")

? }

func gameDidEnd(_ game: DiceGame) {

print("The game lasted for \(numberOfTurns) turns")

? ?}

}

DiceGameTracker 實現了所有 DiceGameDelegate 協議要求的方法。它用三個方法來跟蹤游戲玩了幾輪。它在游戲開始時重置 numberOfTurns 屬性的值為0, 在每次游戲開始的時候加1, 然后在游戲結束后打印出游戲的總輪數。

gameDidStart(:) 函數使用了game 參數來打印一些游戲介紹的信息。game 參數類型是 DiceGame, 而不是 SnakesAndLadders, gameDidStart(:) 可以訪問和使用, 作為DiceGame 協議部分被實現的方法和屬性。不過, 這個方法仍然可以使用類型轉換來查詢潛在實例的類型。在這個例子里, 它會判斷有些是否是一個SnakesAndLadders 實例, 然后打印一條對應的信息。

gameDidStart(:) 方法也會訪問game參數的 dice屬性。因為已經知道 game 遵守了DiceGame 協議, 它可以保證有一個 dice 屬性, 所以 gameDidStart(:) 方法也可以訪問和打印dice 的 sides 屬性, 而不用管正在玩什么類型的游戲。

這里是 DiceGameTracker 的行為:

let tracker = DiceGameTracker()

let game = SnakesAndLadders()

game.delegate = tracker

game.play()

// Started a new game of Snakes and Ladders

// The game is using a 6-sided dice

// Rolled a 3

// Rolled a 5

// Rolled a 4

// Rolled a 5

// The game lasted for 4 turns

給擴展添加協議

你可以擴展一個已存在的類型來采用和遵守一個新的協議, 即使你無法訪問已存在類型的源代碼。擴展可以給已存在的類型添加新屬性,方法和下標, 因此可以添加協議要求的任何需求。

備注:當在擴展里給一個實例類型添加協議時, 這個實例就會自動采用和遵守這個協議。

例如, 這個協議 TextRepresentable, 任何可以表示為文本的類型都可以實現。這可能是自我的描述或者當前狀態的文本版本:

protocol TextRepresentable {

var textualDescription: String { get }

}

早前的 Dice 類可以擴展成采用和遵守 TextRepresentable 協議:

extension Dice: TextRepresentable {

var textualDescription: String {

return "A \(sides)-sided dice"

? ? }

}

擴展采用新協議的方式跟原來Dice提供的實現一樣。協議名寫在類型名后,用冒號隔開, 協議需求的所有實現都寫在擴展的花括號中。

所以 Dice 實例都可以認為是 TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())

print(d12.textualDescription)

// 打印 "A 12-sided dice"

類似的, SnakesAndLadders 也可以擴展成采用和遵守TextRepresentable協議:

extension SnakesAndLadders: TextRepresentable {

var textualDescription: String {

return "A game of Snakes and Ladders with \(finalSquare) squares"

? ? ?} ?

? }

print(game.textualDescription)

// 打印 "A game of Snakes and Ladders with 25 squares"

用擴展聲明協議

如果一個類型符合了協議的所有需求, 但是尚未聲明它采用這個協議, 你可以用一個空的擴展,讓它采用這個協議:

struct Hamster {

var name: String

var textualDescription: String {

return "A hamster named \(name)"

? ? }

}

extension Hamster: TextRepresentable {}

現在需要TextRepresentable 類型的地方都可以使用 Hamster 實例:

let simonTheHamster = Hamster(name: "Simon")

let somethingTextRepresentable: TextRepresentable = simonTheHamster

print(somethingTextRepresentable.textualDescription)

// 打印 "A hamster named Simon"

備注: 僅僅滿足協議的需求,類型還不會自動采用這個協議。必須顯式聲明它們采用了這個協議。

協議類型集合

一個協議可以用作一個類型,存儲在諸如數組或者字典的集合中。下面的例子創建了一組 TextRepresentable:

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

現在可以遍歷數組中的所有項目, 然后打印每一項的文字描述:

for thing in things {

print(thing.textualDescription)

}

// A game of Snakes and Ladders with 25 squares

// A 12-sided dice

// A hamster named Simon

注意 thing 常量的類型是 TextRepresentable. 不是 Dice, 或者 DiceGame, 或者 Hamster類型, 盡管背后是這些類型。 由于它是 TextRepresentable 類型, 它又一個 textualDescription 屬性, 通過循環每次訪問thing.textualDescription 是安全的。

協議繼承

一個協議可以繼承一個或多個其他協議,在繼承的需求頂層還可以添加更多的需求。協議繼承的語法和類繼承的語法很相似, 但是可以選擇列出多個繼承協議, 然后用逗號分開:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {

// protocol definition goes here

}

這里有一個協議的例子,它繼承了上面的 TextRepresentable 協議:

protocol PrettyTextRepresentable: TextRepresentable {

var prettyTextualDescription: String { get }

}

這個例子定義了一個新協議, PrettyTextRepresentable, 它繼承自 TextRepresentable. 任何采用PrettyTextRepresentable 協議的都必須滿足TextRepresentable 強制要求的所有需求, 然后加上PrettyTextRepresentable 強制要求的額外需求。在這個例子里, PrettyTextRepresentable 只添加了一個需求, 提供了一個 gettable 屬性 prettyTextualDescription 來返回一個字符串。

SnakesAndLadders 類可以擴展成采用和遵守 PrettyTextRepresentable 協議:

extension SnakesAndLadders: PrettyTextRepresentable {

var prettyTextualDescription: String {

var output = textualDescription + ":\n"

for index in 1...finalSquare {

switch board[index] {

case let ladder where ladder > 0:

output += "▲ "

case let snake where snake < 0:

output += "▼ "

default:

output += "○ "

? ? ? ? ? ? }

? ? ? ? }

? ?return output

? ? ?}

}

這個擴展表明它采用了 PrettyTextRepresentable 協議,然后為SnakesAndLadders類型提供了prettyTextualDescription屬性的實現。任何是 PrettyTextRepresentable 類型的也是 TextRepresentable 類型, 因此 prettyTextualDescription 實現的開始就是訪問TextRepresentable的屬性 textualDescription,用它作為輸出字符串的開始部分。它往后面添加一個冒號和一個換行符, 然后使用它作為文本的開始。然后它遍歷棋盤方格數組的項目, 然后把代表每個方格的幾何形狀添加到最后:

如果方格的值大于0, 它是梯子的底, 用 ▲表示。

如果方格的值小于0, 它是蛇的頭, 用 ▼表示。

否則, 方格的值等于 0, 這是一個空方格, 用 ○ 表示。

現在 prettyTextualDescription 屬性可以用來為SnakesAndLadders實例打印一條文本描述:

print(game.prettyTextualDescription)

// A game of Snakes and Ladders with 25 squares:

// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

只用于類的協議

通過添加一個class關鍵字到一個協議繼承列表,你可以限制協議只被類采用 (不是結構體也不是枚舉)。class關鍵字應該總是出現在協議繼承列表的第一個, 在任何繼承協議之前:

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {

// class-only protocol definition goes here

}

上面的例子里, SomeClassOnlyProtocol 只能被類采用。如果讓結構體或者枚舉來采用這個協議,會報一個編譯期錯誤。

備注 當協議需求定義的行為假設或者要求符合類型擁有引用語義而非值語義的時候,要使用只用于類的協議。

協議組合

讓一個類型同時符合多個協議是有用的。你可以用一個協議組合把多個協議合并到一個需求里。協議組合的行為就像, 你定義了一個本地臨時協議, 它合并了組合協議的需求。協議組合并不定義任何新的協議類型。

協議組合的形式是SomeProtocol & AnotherProtocol. 你可以列出盡量多你需要的協議, 用 (&) 分開。除了協議列表, 協議組合還可以包含一個類類型, 允許你指定一個所需的基類。

下面有個例子, 合并了兩個協議 Named 和 Aged, 然后把這個協議組合作為一個函數的參數:

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, 有一個 gettable 字符串屬性 name. 然后定義了另外一個協議 Aged, 有一個 gettable 整型屬性 age. 這兩個協議被一個結構體 Person 采用。

這個例子同時定義了一個函數 wishHappyBirthday(to:) , celebrator 參數類型是 Named & Aged, 表示 “符合 Named 和 Aged 協議的任意類型。” 傳入函數的類型不重要, 只要它符合這兩個必須的協議。

這個例子創建了一個新的 Person 實例 birthdayPerson , 然后把這個實例傳入 wishHappyBirthday(to:) 函數。因為 Person 符合這兩個協議, 這是一個有效的調用, wishHappyBirthday(to:) 函數可以打印生日祝福。

下面的例子合并了 Named 協議和上文的 Location 類:

class Location {

var latitude: Double

var longitude: Double

init(latitude: Double, longitude: Double) {

self.latitude = latitude

self.longitude = longitude

? ? }

?}

class City: Location, Named {

var name: String

init(name: String, latitude: Double, longitude: Double) {

self.name = name

super.init(latitude: latitude, longitude: longitude)

? ? }

?}

func beginConcert(in location: Location & Named) {

print("Hello, \(location.name)!")

?}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)

beginConcert(in: seattle)

// 打印 "Hello, Seattle!"

beginConcert(in:) 函數有一個參數, 類型是 Location & Named, 它意味著, 任何 Location 的子類, 只要符合 Named 協議。 在這個例子里, City 滿足這個兩個要求。

如果你嘗試把 birthdayPerson 傳入 beginConcert(in:) 函數, 因為 Person 不是 Location 的子類所以調用無效。同樣, 如果你有一個 Location 子類不符合 Named 協議, 調用 beginConcert(in:) 函數同樣無效。

判斷協議一致性

你可以使用在類型轉換中介紹的 is 和 as 操作符來判斷協議的一致性, 并且轉換成指定的協議。判斷和轉換協議跟判斷和轉換類型的語法完全一致:

如果一個實例符合一個協議, is 操作符返回 true,反之返回 false.

向下轉換操作符 as? 會返回協議類型的一個可選值, 如果這個實例不符合該協議,這個值是 nil.

向下轉換操作符 as! 強制轉換成這個協議,如果轉換失敗會觸發一個運行時錯誤。

下面的例子定義了一個協議 HasArea, 只有一個gettable 要求的浮點型屬性 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 }

}

Circle 類把 area 屬性要求實現成了一個計算屬性, 它基于一個存儲屬性 radius. Country 類直接把 area 屬性需求實現成了一個存儲屬性。兩個類都準確的符合了 HasArea 協議。

這里有一個類 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 數組用三個字面量來初始化,包括半徑為2的 Circle 實例; 用英國表面積初始化的 Country 實例; 有四條腿的 Animal 實例。

現在可以遍歷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. objectWithArea 常量是 HasArea類型, 所以它的 area 屬性可以訪問和安全的打印。

注意,潛在的對象沒有被轉換過程改變。它們依然是一個 Circle, 一個 Country 和一個 Animal. 不過, 在它們被存為 objectWithArea 常量的時候, 它們只是 HasArea 類型, 所有只有它們的 area 屬性才能被訪問。

可選協議需求

你可以為協議定義可選需求, 符合協議的類型不用去實現這些需求。協議定義中,通過前置 optional 修飾符來實現這些需求。可選需求是可用的,這樣你就可以寫代碼和 Objective-C 交互。協議和可選需求都要用 @objc 屬性來標記。注意, @objc 協議只能被繼承自 Objective-C 的類或者 @objc 類采用。不能被結構體和枚舉采用。

當你在可選需求中使用方法或者屬性時, 它的類型自動成為可選。例如, 方法 (Int) -> String 會變成 ((Int) -> String)?. 整個函數都被可選包括,不僅僅是返回值。

可選協議需求可以用可選鏈接來調用, 來說明符合協議的類型不實現這些需求的可能性。調用可選方法的時候,通過在方法名后面寫上問號來判斷它是否實現, 例如 someOptionalMethod?(someArgument).

下面的例子定義了一個整型計數類 Counter, 它用一個外部數據源來提供增長數。這個數據源在 CounterDataSource 協議里定義, 它有兩個可選需求:

@objc protocol CounterDataSource {

@objc optional func increment(forCount count: Int) -> Int

@objc optional var fixedIncrement: Int { get }

}

CounterDataSource 協議定義了一個可選方法需求 increment(forCount:) 和一個可選屬性需求 fixedIncrement. 這些需求為外部數據源定義了兩個不同方法,來為Counter 實例提供一個合適的增長數。

備注 嚴格來說, 你可以寫一個符合CounterDataSource協議的自定義類,卻不用實現它的需求。它們都是可選的。

下面定義的Counter 類, 有一個CounterDataSource?類型的可選的 dataSource 屬性:

class Counter {

var count = 0

var dataSource: CounterDataSource?

func increment() {

if let amount = dataSource?.increment?(forCount: count) {

? ? ? ? count += amount

? ? ? } else if let amount = dataSource?.fixedIncrement {

? ? ? ?count += amount

? ? ? ? ?}

? ? ? ?}

}

Counter 在一個變量屬性count中存儲當前值。同時定義了一個方法increment, 每次方法調用時,它就會增加count屬性的值。

increment() 方法通過數據源的increment(forCount:)方法實現,先嘗試去獲取一個增長數。increment() 方法使用可選鏈接嘗試調用 increment(forCount:), 然后把當前值傳入作為參數。

注意,這里有兩層可選鏈接。首先, dataSource 可能為nil, 所以它后面有個問號,來表明如果dataSource 不是nil,就可以調用 increment(forCount:). 其次, 即使 dataSource 存在, 也不能保證它實現了increment(forCount:), 因為它是一個可選需求。在這里, increment(forCount:) 沒有實現的可能性也是用可選鏈接處理的。只有 increment(forCount:) 方法存在,它才會被調用—就是說, 它不為nil. 這就是為什么 increment(forCount:)名字后也有一個問號的原因。

由于上述兩種原因的任意一個,調用 increment(forCount:) 都有可能會失敗, 所以調用返回一個可選整型值。盡管 increment(forCount:) 在CounterDataSource 中定義的返回值是非可選的,這個也會發生。盡管有兩個可選鏈接操作, 一個接著一個, 結果依然包在一個可選項中。

調用完 increment(forCount:), 返回的可選整型值會拆包到一個常量 amount, 使用可選綁定的方式。如果可選整型值的確包含一個值—就是說, 如果委托和方法都存在, 方法返回一個值—拆包得到的數量會加到存儲屬性count 上, 然后增長完成。

如果不能從increment(forCount:)方法獲取到值—或者是因為 dataSource 為nil, 或者數據源沒有實現 increment(forCount:)—然后 increment() 方法嘗試從數據源的 fixedIncrement 屬性獲取值。fixedIncrement 屬性也是一個可選的需求, 所以它的值是一個可選的整型值。盡管 fixedIncrement 定義時是一個非可選的屬性。

這里有個 CounterDataSource 協議的簡單實現,在這里數據源返回一個常量值3,每當它被查詢的時候。它通過實現fixedIncrement 屬性需求來實現這點:

class ThreeSource: NSObject, CounterDataSource {

let fixedIncrement = 3

? ?}

var counter = Counter()

counter.dataSource = ThreeSource()

for _ in 1...4 {

counter.increment()

print(counter.count)

}

// 3

// 6

// 9

// 12

上面的代碼創建了一個新的 Counter 實例; 然后把數據源設置成新的 ThreeSource 實例; 然后調用 increment() 方法四次。 如預料的一樣, 每次increment()調用, count 屬性都會增加。

這里有一個更復雜的數據源 TowardsZeroSource, 它讓 Counter 實例從當前值向上或者向下往0靠近:

@objc class TowardsZeroSource: NSObject, CounterDataSource {

func increment(forCount count: Int) -> Int {

if count == 0 {

return 0

? ? } else if count < 0 {

? ? ?return 1

? ? } else {

? ?return -1

?}

}

}

TowardsZeroSource 類實現了來自CounterDataSource協議的可選方法 increment(forCount:), 同時使用 count 參數值來判斷往哪個方向計數。如果 count 已經為0, 方法返回0,表明無需繼續計算。

你可以用一個 TowardsZeroSource 實例配合存在的 Counter 實例來計數從-4 到 0. 一旦計數到了0, 計數停止:

counter.count = -4

counter.dataSource = TowardsZeroSource()

for _ in 1...5 {

counter.increment()

print(counter.count)

}

// -3

// -2

// -1

// 0

// 0

協議擴展

協議可以給符合的類型擴展方法和屬性的實現。這讓你可以在協議本身定義行為, 而不是在每個類型的個體或者在一個全局函數。

例如, RandomNumberGenerator 協議能夠擴展提供一個 randomBool() 方法, 它用必須的 random() 方法結果來返回一個隨機的布爾值:

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"

提供默認實現

你可以用協議擴展給協議需求的任何方法和計算屬性提供一個默認實現。如果符合類型提供了必須的方法和屬性的實現, 這個實現會替換擴展提供的實現。

備注 擴展提供的有默認實現的協議需求和可選協議需求是不同的。盡管符合類型沒有必要提供任意一種實現, 帶有默認實現的需求可以不用可選鏈接來調用。

例如, PrettyTextRepresentable 協議, 它繼承 TextRepresentable 協議,可以提供prettyTextualDescription 屬性的默認實現,然后簡單返回訪問textualDescription屬性的結果:

extension PrettyTextRepresentable? {

var prettyTextualDescription: String {

return textualDescription

? ?}

}

給協議擴展添加限制

當你定義了一個協議擴展, 你可以指定限制,符合類型在擴展的方法和屬性可用前,必須滿足這些限制。在協議名后面用where子句寫上限制。

例如, 你可以給Collection 協議定義一個擴展, 它可以用于任何元素符合 TextRepresentable 協議的集合類型。

extension Collection where Iterator.Element: TextRepresentable {

var textualDescription: String {

let itemsAsText = self.map { $0.textualDescription }

return "[" + itemsAsText.joined(separator: ", ") + "]"

? ? }?

}

通過把集合里的每個元素的文本描述連接在一起, textualDescription 屬性返回整個集合的文本描述, 然后用方括號括起來。

考慮前面的結構體 Hamster, 它符合 TextRepresentable 協議, 然后是一個 Hamster 值的數組:

let murrayTheHamster = Hamster(name: "Murray")

let morganTheHamster = Hamster(name: "Morgan")

let mauriceTheHamster = Hamster(name: "Maurice")

let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]

因為數組符合 Collection 協議,并且數組的元素符合 TextRepresentable 協議, 所以數組可以用 textualDescription 屬性獲取它內容的文本表示:

print(hamsters.textualDescription)

// 打印 "[A hamster named Murray, A hamster named Morgan, A hamster named Maurice]"

備注 如果一個符合類型滿足了多個限制表達式的要求, Swift 會使用對應特定限制的實現。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 126.析構器 在一個類實例銷毀前,一個析構器會立即調用。使用deinit 關鍵字來表示析構器, 跟構造器寫法類似...
    無灃閱讀 840評論 0 4
  • 123.繼承 一個類可以從另外一個類繼承方法,屬性和其他特征。當一個類繼承另外一個類時, 繼承類叫子類, 被繼承的...
    無灃閱讀 1,430評論 2 4
  • 136.泛型 泛型代碼讓你可以寫出靈活,可重用的函數和類型,它們可以使用任何類型,受你定義的需求的約束。你可以寫出...
    無灃閱讀 1,512評論 0 4
  • SwiftDay011.MySwiftimport UIKitprintln("Hello Swift!")var...
    smile麗語閱讀 3,857評論 0 6
  • 人們常說老師培養著祖國的棟梁;作為家長,我卻覺得老師就是祖國的棟梁。正是老師,支撐起一代代人的脊梁。在我的孩子學習...