首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》4.8 协议

关灯直达底部

协议是一种表示不相关类型共性的方式。比如,Bee对象与Bird对象可能有一些共性,因为蜜蜂与鸟都能飞。因此,定义一个Flier类型会好一些;但问题在于:让Bee与Bird都成为Fliers会有多大的意义呢?

当然了,一种可能是使用类继承。如果Bee与Bird都是类,那就存在一种父类与子类的类继承。这样,Flier就是Bee与Bird的父类。问题在于,可能存在其他一些原因使得Flier不能作为Bee与Bird的父类。Bee是个Insect,而Bird不是;但它们都可以飞,这是彼此独立的能力。我们需要一种类型可以某种方式透过类继承体系,将不相关的类集成到一起。

此外,如果Bee与Bird都不是类该怎么办呢?在Swift中,这是非常有可能的。重要且强大的对象可以是结构体而非类,不过并不存在父结构体与子结构体的结构体层次体系。毕竟,这是结构体与类之间的一个主要差别。但结构体也需要像类一样拥有和表达正常的共性特性。Bee结构体与Bird结构体怎么可能都是Fliers呢?

Swift通过协议解决了这一问题。协议在Swift中是非常重要的;Swift头文件中定义了70多个协议!此外,Objective-C也支持协议;Swift协议大体上与Objective-C协议一致,并且可以与之交换。Cocoa大量使用了协议。

协议是一种对象类型,不过并没有协议对象——你无法实例化协议。协议要更加轻量级一些。协议声明仅仅是一些属性与方法列表而已。属性没有值,方法没有代码!其想法是“真实”的对象类型可以声明它属于某个协议类型;这叫作使用或遵循协议。使用协议的对象类型会遵守这样一个契约:它会实现协议所列出的属性与方法。

比如,假设成为Flier需要实现一个fly方法;那么,Flier协议可以指定必须要有一个fly方法;为了做到这一点,它会列出fly方法,但却没有函数体,如下代码所示:


protocol Flier {    func fly}  

任何类型(枚举、结构体、类,甚至是另一个协议)都可以使用该协议。为了做到这一点,它需要在声明中的名字后面加上一个冒号,后跟协议名(如果使用者是个拥有父类的类,那么父类后面还需要加上一个逗号,协议则位于该逗号后面)。

假设Bird是个结构体,那么它可以像下面这样使用Flier:


struct Bird : Flier {} // compile error  

目前来看一切都没问题,不过上述代码无法编译通过。Bird结构体承诺要实现Flier协议的特性,现在它必须要履行承诺!fly方法是Flier协议的唯一要求。为了满足这一点,我在Bird中增加了一个空的fly方法:


protocol Flier {    func fly}struct Bird : Flier {    func fly {    }}  

这么做就没问题了!我们定义了一个协议,并且让一个结构体使用该协议。当然了,在实际开发中,你可能希望使用者对协议方法的实现能够做一些事情;不过,协议对此并没有做任何规定。

在Swift 2.0中,协议可以声明方法并提供实现,这要归功于协议扩展,本章后面将会对此进行介绍。

4.8.1 为何使用协议

也许到这个时候你还不太理解协议到底有什么用。我们让Bird成为一个Flier,然后呢?如果想让Bird知道如何飞,为什么不在Bird中声明一个fly方法,这样就无须使用任何协议了。这个问题的答案与类型有关。别忘了,协议是一种类型;我们的协议Flier是一种类型。因此,我可以在需要类型的时候使用Flier。比如,可以用它声明变量的类型,或函数参数的类型:


func tellToFly(f:Flier) {    f.fly}  

仔细想想上面的代码,因为它体现了协议的精髓。协议是一种类型,因此适用于多态。协议赋予我们表达类与子类概念的另一种方式。这意味着,根据替代法则,这里的Flier可以是任何对象类型的实例:枚举、结构体或类。对象类型是什么不重要,只要它使用了Flier协议即可。如果使用了Flier协议,那么它就会有fly方法,因为这是使用Flier协议所要求的!因此,编译器允许我们向该对象发送fly消息。根据定义,Flier是个可以接收fly消息的对象。

不过,反过来就不行了;拥有fly方法的对象不一定就是Flier。它不一定遵循了协议的要求;对象类型必须要使用协议。如下代码将无法编译通过:


struct Bee {    func fly {    }}let b = BeetellToFly(b) // compile error  

Bee可以接收fly消息,这是以Bee的身份做的。不过,tellToFly并不接收Bee参数;它接收的是Flier参数。形式上,Bee并非Flier。要想让Bee成为Flier,只需形式上声明Bee使用了Flier协议。如下代码可以编译通过:


struct Bee : Flier {    func fly {    }}let b = BeetellToFly(b)  

关于鸟与蜜蜂的示例到此为止,下面来看看实际的示例吧!如前所述,Swift已经提供了大量的协议,下面让我们自定义的类型使用其中一个协议。Swift提供的最有用的协议之一是CustomStringConvertible。CustomStringConvertible协议要求我们实现一个description String属性。如果这么做了,那就会有奇迹发生:在将该类型的实例用在字符串插入或print中时(或是控制台中的po命令),description属性就会自动用来表示该实例。

回忆一下本章之前介绍的Filter枚举,我向其中添加一个description属性:


enum Filter : String {    case Albums = "Albums"    case Playlists = "Playlists"    case Podcasts = "Podcasts"    case Books = "Audiobooks"    var description : String { return self.rawValue }}  

不过,这么做还不足以让Filter具备CustomStringConvertible协议的功能;要想做到这一点,我们还需要正式使用CustomStringConvertible协议。Filter声明中已经有了一个冒号与类型,因此所使用的协议需要放在逗号后面:


enum Filter : String, CustomStringConvertible {    case Albums = "Albums"    case Playlists = "Playlists"    case Podcasts = "Podcasts"    case Books = "Audiobooks"    var description : String { return self.rawValue }}  

现在,Filter已经正式使用CustomStringConvertible协议了。CustomStringConvertible协议要求我们实现一个description String属性;我们已经实现了一个description String属性,因此代码可以编译通过。现在可以向print传递一个Filter或将其插入到一个字符串中,其description将会被自动打印出来:


let type = Filter.Albumsprint(type) // Albumsprint("It is /(type)") // It is Albums  

看到协议的强大威力了吧,你可以通过相同方式为任何对象类型赋予字符串转换的能力。

一个类型可以使用多个协议!比如,内建的Double类型就使用了CustomStringConvertible、Hashable、Comparable和其他内建协议。要想声明使用多个协议,请在声明中将每个协议列在第一个协议后面,中间用逗号分隔。比如:


struct MyType : CustomStringConvertible, Hashable, Comparable {    // ...}  

(当然,除非在MyType中声明所需的方法,否则上述代码将无法编译通过;声明完之后,MyType就真正使用了这些协议)。

4.8.2 协议类型测试与转换

协议是一种类型,协议的使用者是其子类型,这里使用了多态。因此,用于对象真实类型的那些运算符也可以用于声明为协议类型的对象。比如,Flier协议被Bird与Bee使用了,那么我们就可以通过is运算符测试某个Flier是否为Bird:


func isBird(f:Flier) -> Bool {    return f is Bird}  

与之类似,as!与as?可用于将声明为协议类型的对象向下转换为其真正的类型。这是非常重要的,因为使用协议的对象可以接收协议无法接收的消息。比如,假设Bird有个getWorm方法:


struct Bird : Flier {    func fly {    }    func getWorm {    }}  

Bird能以Flier身份fly,但却只能以Bird身份getWorm,你不能让任意一个Flier去getWorm:


func tellGetWorm(f:Flier) {    f.getWorm // compile error}  

不过,如果这个Flier是个Bird,那么它显然可以getWorm,这正是类型转换要做的事情:


func tellGetWorm(f:Flier) {    (f as? Bird)?.getWorm}  

4.8.3 声明协议

只能在文件顶部声明协议。要想声明协议,请使用关键字protocol,后跟协议名;作为一种对象类型,协议名首字母应该是大写的。接下来是一对花括号,里面可以包含如下内容:

属性

协议中的属性声明包含了var(不是let)、属性名、冒号、类型,以及包含单词get或get set的一对花括号。对于前者来说,使用者对该属性的实现是可写的;对于后者来说,它需要满足如下规则:使用者不可以将get set属性实现为只读计算属性或常量(let)存储属性。

要想声明静态/类属性,请在前面加上关键字static。类使用者可以将其实现为类属性。

方法

协议中的方法声明是个没有函数体的函数声明,即没有花括号,因此也没有代码。任何对象函数类型都是合法的,包括init与下标(在协议中声明下标的语法与在对象类型中声明下标的语法是相同的,只不过没有函数体,就像协议中的属性声明一样,它也可以包含get或get set)。

要想声明静态/类方法,请在前面加上关键字static。类使用者可以将其实现为类方法。

如果方法(由枚举或结构体实现)想要声明为mutating,那么协议就必须指定mutating指令;如果协议没有指定mutating,那么使用者将无法添加。不过,如果协议指定了mutating,那么使用者可以将其省略。

类型别名

协议可以通过声明类型别名为声明中的类型指定局部同义词。比如,通过typealias Time=Double可以在协议花括号中使用Time类型;在其他地方(比如,使用协议的对象类型中)则不存在Time类型,不过可以使用Double类型。

在协议中还可以通过其他方式使用类型别名,稍后将会介绍。

协议使用

协议本身还可以使用一个或多个协议;语法与你想象的一样,声明中的协议名后面是一个冒号,后面跟着它所使用的协议列表,中间用逗号分隔。事实上,这种方式创建了一个二级类型层次!Swift头文件中大量充斥了这种用法。

出于清晰的目的,使用了另一个协议的协议可以重复被使用的协议花括号中的内容,但不必这么做,因为这种重复是隐式的。使用了这种协议的对象类型必须要满足该协议以及该协议使用的所有协议的要求。

如果协议的唯一目的是将其他协议组合起来,但不会添加任何新功能,并且这种组合仅仅用在代码中的一个地方,那么可以通过即时创建组合协议以避免声明协议。要想做到这一点,请使用类型名protocol<...,...>,其中尖括号中的内容是个逗号分隔的协议列表。

4.8.4 可选协议成员

在Objective-C中,协议成员可以声明为optional,表示该成员不必被使用者实现,但也可以实现。为了与Objective-C保持兼容,Swift也支持可选协议成员,不过只用于显式与Objective-C桥接的协议,方式是在声明前加上@objc属性。在这种协议中,可选成员(方法或属性)是通过在声明前加上optional关键字实现的:


@objc protocol Flier {    optional var song : String {get}    optional func sing}  

只有类可以使用这种协议,并且符合如下两种情况之一才能使用该特性:类是NSObject子类,或者可选成员被标记为@objc特性:


class Bird : Flier {    @objc func sing {        print("tweet")    }}  

可选成员不保证会被使用者实现,因此Swift并不知晓向Flier发送song消息或sing消息是否安全。

对于song这样的可选属性来说,Swift通过将其值包装到Optional中来解决这个问题。如果Flier使用者没有实现该属性,那么结果就是nil,并不会出现什么问题:


let f : Flier = Birdlet s = f.song // s is an Optional wrapping a String   

这是很少会出现的要使用双重Optional的一种情况。比如,如果可选属性song的值是个String?,那么从Flier中获取其值就会得到一个String??。

可选属性可以由协议声明为{get set},不过并没有相关的语法可以设置该协议类型对象中的这种属性。比如,如果f是个Flier,其song被声明为{get set},那么你就不能设置f.song。我认为这是语言的一个Bug。

对于像sing这样的可选方法来说,事情将变得更为复杂。如果方法没有实现,那么我们就不可以调用它。为了解决这一问题,方法本身会被自动变成其所声明类型的Optional版本。因此,要想向Flier发送sing消息,你需要将其展开。安全的做法是以可选的方式展开它,使用一个问号:


let f : Flier = Birdf.sing?  

上述代码可以编译通过,也可以安全地运行。效果相当于只有当f实现了sing时才向其发送sing消息。如果使用者的实际类型并未实现sing,那么什么都不会发生。虽然可以强制展开调用(f.sing!()),不过如果使用者没有实现sing,那么应用将会崩溃。

如果可选方法返回一个值,那么它也会被包装到Optional中。比如:


@objc protocol Flier {    optional var song : String {get}    optional func sing -> String}  

如果现在在Flier上调用sing?(),那么结果就是一个包装了String的Optional:


let f : Flier = Birdlet s = f.sing? // s is an Optional wrapping a String  

如果强制展开调用(sing!()),那么结果要么是一个String(如果使用者实现了sing),要么应用崩溃(如果使用者没有实现sing)。

很多Cocoa协议都有可选成员。比如,iOS应用会有一个应用委托类,它使用了UIApplicationDelegate协议;该协议有很多方法,所有方法都是可选的。不过,这对如何实现这些方法是没有影响的;你无须通过任何特殊的方式标记它们。应用委托类已经是NSObject的子类,因此该特性可以正常使用,无论是否实现了方法都如此。与之类似,你常常会让UIViewController子类使用带有可选成员的Cocoa委托协议;它也是NSObject的子类,因此你只需实现想要实现的那些方法,不必做任何特殊的标记。(第10章将会深入介绍Cocoa协议,第11章则会深入介绍委托协议。)

4.8.5 类协议

名字后面的冒号后使用关键字class声明的协议是类协议,表示该协议只能由类对象类型使用:


protocol SecondViewControllerDelegate : class {    func acceptData(data:AnyObject!)}  

(如果协议已经被标记为@objc,那就无须使用class;@objc特性隐含表示这还是个类协议。)

声明类协议的典型目的在于利用专属于类的内存管理特性。目前还没有介绍过内存管理,不过还是先给出示例吧(第5章介绍内存管理时还会探讨这个主题)。


class SecondViewController : UIViewController {    weak var delegate : SecondViewControllerDelegate?    // ...}  

关键字weak标识delegate属性将会使用特殊的内存管理,只有类实例可以使用这种特殊的内存管理。delegate属性的类型是个协议,而协议可以由结构体或枚举类型使用。为了告诉编译器该对象实际上是个类实例而非结构体或枚举实例,这里的协议被声明成了类协议。

4.8.6 隐式必备初始化器

假设协议声明了一个初始化器,同时一个类使用了该协议。根据协议的约定,该类及其子类必须要实现这个初始化器。因此,该类不仅要实现该初始化器,还要将其标记为required。这样,声明在协议中的初始化器就是隐式必备的,而类则需要显式满足这个要求。

下面这个简单的示例是无法通过编译的:


protocol Flier {    init}class Bird : Flier {    init {} // compile error}  

上述代码会产生一段详细且信息丰富的编译错误消息:“Initializer requirement init()can only be satisfied by a required initializer in non-final class Bird.”要想让代码编译通过,我们需要将初始化器指定为required。


protocol Flier {    init}class Bird : Flier {    required init {}}  

正如编译错误消息所示,我们可以将Bird类标记为final。这意味着它不能有任何子类,从而确保这个问题不会再出现。如果将Bird标记为final,那就没必要将init标记为required了。

在上述代码中,我们并未将Bird标记为final,但其init被标记为了required。如前所述,这意味着如果Bird实现了指定初始化器(从而丧失了初始化器的继承),那么其子类就必须要实现必备初始化器,并将其标记为required。

该解决方案用于处理本章之前提到的Swift iOS编程中一个奇怪、恼人的特性。假设继承了内建的Cocoa类UIViewController(很多时候你都会这么做),并且为子类添加了一个初始化器(很多时候你也会这么做):


class ViewController: UIViewController {    init {        super.init(nibName: "ViewController", bundle: nil)    }}  

上述代码无法编译通过,编译器会报错:“required initializer init(coder:)must be provided by subclass of UIViewController.”

我们需要理解所发生的事情。UIViewController使用了协议NSCoding。该协议需要一个初始化器init(coder:)。不过,这些都不是你做的;UIViewController与NSCoding是由Cocoa而不是你声明的。但这都没关系!这与上述情况一样。你的UIViewController子类要么继承init(coder:),要么显式实现它并将其标记为required。由于子类已经实现了自己的指定初始化器(从而丧失了初始化器继承),因此它还需要实现init(coder:)并将其标记为required!

不过,如果不希望在UIViewController子类中调用init(coder:),这样做就没什么用了。这么做只不过是提供了一个没什么用处的初始化器而已。幸好,Xcode的Fix-It特性会帮助你生成这个初始化器,如下代码所示:


required init?(coder aDecoder: NSCoder) {    fatalError("init(coder:) has not been implemented")}  

上述代码符合编译器的要求。(第5章将会介绍为什么说它不符合初始化器的契约,但还是一个合法的初始化器。)如果调用这个初始化器,那么程序就会崩溃,这是有意而为之的。

如果希望这个初始化器完成一些功能,那么请删除fatalError这一行,然后插入自己的功能实现代码。一个有意义且代码量最小的实现是super.init(coder:aDecoder);当然,如果类有需要初始化的属性,那就需要先初始化它们。

除了UIViewController,还有很多内建的Cocoa类都使用了NSCoding。在继承这些类并实现自己的初始化器时就会遇到这个问题,你得习惯才行。

4.8.7 字面值转换

Swift的精妙之处在于,相对于内建以及魔法实现,它的很多特性都是由Swift本身实现的,并且可以通过Swift头文件一探究竟,字面值就是这样的。相对于通过Int(5)来初始化一个Int,你可以直接将5赋给它,其原因并不是来自于什么神奇魔法,而是因为Int使用了协议IntegerLiteralConvertible。除了Int字面值,所有字面值均如此。如下字面值转换协议都声明在Swift头文件中:

·NilLiteralConvertible

·BooleanLiteralConvertible

·IntegerLiteralConvertible

·FloatLiteralConvertible

·StringLiteralConvertible

·ExtendedGraphemeClusterLiteralConvertible

·UnicodeScalarLiteralConvertible

·ArrayLiteralConvertible

·DictionaryLiteralConvertible

你自己定义的对象类型也可以使用字面值转换协议,这意味着可以在需要对象类型实例的情况下使用字面值!比如,下面声明了一个Nest类型,它包含了一些鸡蛋(即eggCount):


struct Nest : IntegerLiteralConvertible {    var eggCount : Int = 0    init {}    init(integerLiteral val: Int) {        self.eggCount = val    }}  

由于Nest使用了IntegerLiteralConvertible,我们可以在需要Nest的地方使用Int,init(integerLiteral:)会自动调用,这会创建一个具有指定eggCount的全新Nest对象:


func reportEggs(nest:Nest) {    print("this nest contains /(nest.eggCount) eggs")}reportEggs(4) // this nest contains 4 eggs