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 会使用对应特定限制的实现。
网友评论