Swift之协议

Posted 喵渣渣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之协议相关的知识,希望对你有一定的参考价值。

前言

如果你之前使用objective-c编写ios程序肯定对协议都不陌生,在Swift中苹果将protocol这种语法发扬的更加深入和彻底。Swift中的protocol不仅能定义方法还能定义属性,配合extension扩展的使用还能提供一些方法的默认实现,而且不仅类可以遵循协议,现在的枚举和结构体也能遵循协议了。

协议中定义范型,属性,方法

  • 协议的定义
    协议为方法,属性,以及其他特性的任务需求或功能提供了一个入口。协议可以背类,结构体,或枚举类型采纳以提供所需功能的具体实现。满足了协议中虚啊哟的任意类型都叫做遵循了该协议。
    Swift中定义一个协议和定义枚举,结构体或者类的格式都是相同的,使用protocol关键字:
// 定义一个名字为动物的协议
protocol Animal 

这里 Animal是使用 protocol 关键字声明的一个协议,和枚举结构体类命名原则相似,Animal 首字母大写表示在以后使用的过程中 Animal看作是一个类型使用。

  • 协议中定义范型
    Swift中的协议(protocol)采用的是 “Associated Types“的方式来实现范型功能的,通过associatedtype 关键字来声明一个类型的占位符作为协议定义的一部分。Swift不支持下面的定义方式:
 protocol GeneratorType 
    public mutating func next() -> Element?

而是应该使用这样的定义方式

protocol GeneratorType 
        associatedtype Element
        public mutating func next() -> Selef.Element
    

在Swift中,class,struct,enums都可以是用参数化类型来表达泛型的,只有在协议中使用associatedtype关键字来表达参数化类型。为什么协议不采用这样的语法形式呢?我查看了很多讨论,原因大概总结有以下两点:

1.采用语法的参数化方式的泛型其实定义了整个类型的家族,在概念上这对于一个可以具体实现的类型(class,struct,enums)都是有意义的,比方说Arrayy。但是对于协议来说,协议表达的含义是single的,你只会实现一个GeneratorType,而不会实现一个GeneratorType协议,接着又实现另外一个GeneratorType协议。

2.协议在Swift中有两个目的,第一个目的是用来实现多继承(Swift
语言被设计成为单继承的),第二个目的是强制实现者必须遵守自己所定的泛型约束。关键字associatedtype是用来实现第二个目的的。在GeneratorType中由associatedtype指定的Element, 是用来控制next()方法的返回类型的。而不是用来GeneratorType的类型的

  • 协议中定义属性

    协议中定义属性表示遵循改协议的类型具备了某些属性,具体来说只能使用var 关键字声明并且必须明确规定该属性是可读的get ,还是可读可写的get set ,另外还可以通过关键字static 声明一个类型属性:

protocol Animal 
    // 定义一个可读可写的 name属性
    var name: String get set
    // 定义一个可读的age属性
    var age: Int get
    //定义一个类属性 hometown
  static var hometown: String get
 

和定义方法一样,我们只需要确定该属性具体是什么类型并且添加对应的关键字,不需要具体的实现,更不能为它们赋上初始值(类似于计算型属性)。定义好属性之后,我们就可以利用属性来做点事情了

Struct Cat: Animal 
    static var hometown: String = "上海"
    var name: String
    var age: Int = 1

var miaomiao = Cat(name: "小黑", age: 2)

定义一个Cat 结构体遵循 Animal 协议, 该结构体中必须存在协议要求声明的两个属性 name,agestatic 修饰的类型属性必须被有初始化值或者存在get, set 方法。对于普通的实力属性协议并不关心是计算型属性还是存储型属性。实例中的属性同样可以被修改:

var miaomiao = Cat(name: "小黑", age: 2)
Cat.hometown = "北京"

看到这里有的同学可能有些疑问, agehometown 明明只有 get 方法为什么却可以修改赋值呢?其实协议中的“只读“属性修饰的是协议这种“类型“的实例,例如下面的例子:

var mimi:  Cat = miaomiao
mimi.age = 3

虽然我们并不能像创建类的实例那样直接创建协议的实例,但是我们可以通过”赋值”得到一个协议的实例。将miaomiao 的值赋值给 Cat 类型的变量 mimi, 修改 mimiage 属性时编译器就会报错: age 是一个只读的属性,不能被修改。 如果Cat 中还存在 Animal 没有的属性, 那么赋值过程中 mimi 就将不会存在这样的属性,尽管这样做的意义并不大,但是我们从中知道了协议中 get , set 的具体含义。

  • 协议中定义的方法

和objective-c类型,Swift中的协议可以定义类型方法或者实例方法,方法的参数不能有默认值(Swift认为默认值也是一种变相的实现), 在遵守该协议的类型中具体实现方法的细节,通过类或实例调用:

protocol Student 
    // 类方法
    static func study()
    // 实例方法
    func changeName() 

struct CollageStudent: Student 
    // 类方法实现
    static func study() 
    
    // 实例方法实现
    func changeName() 
    

// 方法的调用
CollageStudent.study()
var cl = CollageStudent()
cl.changeName()

注意: 当我们在结构体中的方法修改到属性的时候,需要在方法前面加上关键字 mutaing 表示该属性能够被修改 (如果是类不需要添加 mutating 关键字), 这样的方法叫做: 异变方法, 和“在实例方法中修改值类型“的处理是一样的。

protocol Student 
    mutating func changeName()

struct CollageStudent: Student     
    mutating func changeName() 
        self.name = "小明"
  • 协议中的初始化器
    我们可以在协议中国定义遵循协议的类型需要实现的指定初始化器(构造函数)或者便捷初始化器。
protocol Pet 
    init(name: String)

class Cat: Pet 
  var name: string = "Cat"
  required init(name: String) 
      self.name = name 
  

Cat 由于遵循类 Pet 协议,应该用 required 关键字修饰初始化器的具体实现。
如果一个类既继承了某个类,而且遵循了一个或多个协议,我们应该将父类放在最前面,然后依次用逗号排列。

class SomeClass: OneProtocol, TwoProtocol 

这是因为Swift中类的继承是单一的,但是可以遵守多个协议,因此为了突出其单一父类的特殊性,我们应该将继承的父类放在最前面,将遵守的协议依次放在后面

  • 多个协议重名方法调用冲突

由于在swift中并没有规定不同的协议内方法不能重名(这样的规定也是不合理的)。因此我们在自定义多个协议中方法重名的情况是可能出现的,比如有TextOne, TextTwo 两个协议,定义如下:

protocol TextOne 
    func text() -> Int 

protocol TextTwo 
    func text() -> Int 

这两个协议中的 text() 方法名相同返回值不同, 如果存在一个类型Person 同时遵守了TextOneTextTwo ,在 Person 实例调用方法的时候就会出现歧义。

struct Person: TextOne, TextTwo 
    func text() -> Int 
        return 10
    
    func text() -> Int 
        return "hellow"
    

let p1 = Person()
// 尝试调用返回值为Int的方法
let num = p1.text()
// 尝试调用返回值为String 的方法
let string = p1.next()

上面的调用肯定是无法通过的,因为编译器无法得知同名 text() 方法到底是哪个协议中的方法,那么出现这种情况的根本原因在于调用哪个协议的 text() 不确定,因此我们需要指定调用特定协议的text() 方法,改进后的代码如下:

// 尝试调用返回值为Int的方法
let num = (p1 as TextOne).text()
// 尝试调用返回值为String的方法
let string = (p1 as TextTwo).text()

也可以理解为在进行调用前将 p1 常量进行类型转换。

协议的继承、聚合、关联类型

  • 协议的继承

协议可以继承一个或者多个其他协议并且可以在它继承的基础之上添加更多要求。协议继承的语法与类机场的语法相似,选择列出多个继承的协议,使用逗号分隔:

protocol OneProtocol 

protocol TwoProtocol 

// 定义一个继承子OneProtocol 和 TwoProtocol协议的新协议: ThreeProtocol
protocol ThreeProtocol: OneProtocol, TwoProtocol 

如上所示,任何遵守了ThreeProtol 协议的类型都应该同时实现 OneProtocolTwoProtocol 要求必须实现的方法或属性。

  • 协议的聚合

日常开放中要求一个类型同时遵守多个协议是很常见的,除了使用协议的继承外,我们还可以使用形如OneProtocol & TwoProtocol 的形式实现协议聚合(组合)复合多个协议到一个要求里。例如:

// 协议聚合成临时的类型
typealias Three = TwoProtocol & OneProtocol
// 协议聚合成为参数的类型
func text(paramter: OneProtocol & TwoProtocol) 

一个很常见的例子:定义text 函数的参数类型使用了协议的聚合,在这里我们并不关系paramater 是什么类型的参数,只要它遵循这两个要求的协议即可。

  • 继承和聚合在使用上的区别

善于思考的人可以发现,要实现上面的paramter 参数的类型是遵守 OneProtocolTwoProtocol 的效果,完全可以使用协议的继承,新定义一个ThreeProtocol 继承自 OneProtocolTwoProtocol ,然后指定 paramter 参数的类型是 ThreeProtocol 类型。那么这么连个方法有何区别呢?首先协议的继承是定义了一个全新的协议,我们死希望它能后“大展拳脚”的到普遍使用。而协议的聚合不一样,它并咩有定义新的固定协议类型,相反,它只是定义一个临时的拥有所有聚合协议要求组成的局部协议,很可能是“一次性需求”,使用协议的聚合保持了代码的简洁性、易读性,同时去除了定义不必要的新类型的繁琐,并且定义和使用的地方如此接近,见名知意,也就是匿名协议聚合。但是使用了匿名协议聚合能够表达的信息就少了一些,所以需要开发中斟酌使用。

  • 协议的检查

如何检查某个类型是否遵循了特定的协议? : 使用关键字 is ,同时该运算符会返回一个Bool值用于判断条件是否成立。

Struct Person: OneProtocol 

let p1 = Person()
if pi is OneProtocol 
    // 可以理解为:p1是一个遵守了Oneprotocol协议类型的实例 
    print("yes")

如何让定义的协议只能被类遵守? : 使用关键字 class 该关键字修饰后表示协议只能被类遵守,如果有枚举或者结构体尝试遵守就会报错。

// 只能被类遵守的协议
protocol FourProtocol: class ThreeProtocl 

// 此处报错
struct Person: FourProtocol 

// 此处通过
class Person: FourProtocol 
  • 关联类型

协议的关联类型值的是根据使用场景的变化,如果协议中某些属性存在“逻辑相同的而类型不同的情况”,可以使用关键字associatedtype 来为这些属性的类型声明“关联类型”。

protocol WeightCalculable 
    // 为weight属性定义的类型别名
    associatedtype WeightType
    var weight: WeightType get

WeightCalculable 是一个“可称重”协议,weight 属性返回遵守该协议具体类型的实例的重量。这里我们使用associatedtype 为该属性的类型定义了一个别名 WeightType ,换言之在WeightCalculable 中并不关心weight 的类型是 Int 还是 Double 或则其他类型,它只是简单的告诉我们类型是WeightType ,至于WeightType 到底是什么类型由遵守该协议的类中自己去定义。那么这样做的好处是什么呢?

// 定义手机结构体
struct MobilePhone: WeightCalculable 
typealias WeightType = Double
var weight: WeightType

let iPhone7 = MobilePhone(weight: 0.138)
// 定义汽车结构体
struct Car: WeightCalculable 
    typealias: WeightType = Int
    var weight: WeightType

let truck = Car(weight: 3000_000)

上所示:MobilePhoneCar 类型都遵守了WeightCalculable 协议,都能被称重,但是手机由于结构精密、体型小巧,小数点后面的数字对于称重来说是必不可少的,所以Double 类型,返回0.138千克138克 ,但是对于汽车这么庞然大物在称重时,如果还计较小数点后面的数字就显得没有意义了,所以使用Int 类型,所以使用Int类型,表示3000千克 也即是3吨

从上面的例子可以很好的看出由于MobilePhoneCar 称重时逻辑是一样的,但是对于Weight 属性的返回值要求不一样,如果仅仅因为返回值类型的不同定义两个类似的协议,一个是Int类型,另一个是Double类型,这样做显然是重复的、不合适的。所以associatedtype在这种情况下就发挥出作用了,他让开发者在遵守协议时根据需要去定义返回值的类型,而不是在协议中写死。唯一要注意的是:一定要在遵守该协议的类型中使用typealias规定具体的类型。不然编译器就报错了。

协议的扩展

协议的扩展是协议中很重要的一部分内容,具体体现在以下两个方面:

  • 扩展协议的属性和方法

我们通过一个常见的例子说明以下:

protocol Score 
    var math: Intget set
    var english: Intget set
    func mathPercent() -> Double

首先定义一个Score协议,里面有两个Int类型的属性mathenglish和一个计算数学所占分数的比例的方法mathPercent.

struct Puple: Score 
    var math: Int
    var english: Int
    func mathPercent() -> Double 
        return Double(math)/Double(math+english)
    

定义Puple遵守该协议,实现了必要的属性和方法

let p1 = Puple(math:90, english:80)
s1.mathPercent()

通过上面的代码可以计算出s1中数学所占的比例,但是设想一下如果还有类似的Puple结构体的类型都需要遵守该协议,都需要默认实现mathPercent方法计算出自己的数学分数所占的比例,还是按照上面的写法代码量就很大而且很冗杂了。问题的关键在于:任何遵守Score协议类型的mathPercent计算逻辑是不变的,而且需要默认实现。那么我们如何轻松的实现这样的效果呢,答案是:为Score添加方法的扩展。

extension Score 
    func mathPercent() -> Double 
        return Double(math)/Double(math+english)
    

mathPercent 的具体试下写在协议的扩展里面,就能为所有的遵守Score的类型提供mathPercent默认的实现。

struct CollageStudent: Score 
    var math: Int
    var english: Int

let c1 = CollageStudent(math: 80, english: 80)
c1.mathPercent()

如此就能起到“不实现mathPercent方法也能计算出数学所占分数的比例”的效果了。此语法在Swift中有一个专业术语叫做:default implementation即默认实现。包括计算属性和方法的默认实现,但是不支持存储型属性,如果遵循类型给这个协议的要求提供了它自己的实现,那么它就会替代扩展中提供的默认实现。通过这样的语法,我们不仅能为自定义的协议提供扩展,还能为系统提供的协议添加扩展,例如,为CustomStringConvertible添加一个计算属性默认实现的扩展:

extension CustomStringConvertible 
    var customDescription: String 
        return "YQ" + description
    
  • 为存在的类型添加协议遵守

扩展一个已经存在的类型来采纳和遵守一个新的协议,无需访问现有类型的源代码。扩展可以添加新的属性、方法和下标到已经存在的类型,并且因此允许你添加协议需要的任何需求。

简单的来说我们可以对存在的类型(尤其是系统的类型)添加协议遵守。尽管这更像是对“类型的扩展”,但是官方文档将这部分放在了协议的扩展中。

extension Double: CustomStringConvertible 
    // 一个值的文本描述
    public var description: String get

上面的代码就是Swift标准库中对于Double类型添加的一个协议遵守。除了添加系统协议的遵守,我们还可以添加自定义的协议的遵守,其方法都是一样的,这里就不赘述了。

  • 总结

通过协议的廓镇提供协议中某些属性和方法的默认实现,将公共的代码和属性统一起来极大增加了代码的服用,同时也增加了协议的灵活性和使用范围,这样的协议不仅仅是一系列接口的规范,还能提供相应的逻辑,是面向协议编程的基础。

Swift标准库中常见的协议

学习完协议的基础语法,我们大致熟悉一下Swift标准库中提供的协议。

55个标准库协议

Swift标准库为我们提供了55种协议,他们的命名很有特点,基本是以“Type”、“able”、“Convertible”结尾,分别表示该协议“可以被当做XX类型”、“具备某种能力或者、特性”、“能够进行改变或变换”。因此在自定义协议时应该尽可能遵守苹果的命名规则,便于开发人员之间的高效合作。下面介绍一下常见的几种协议:

  • Equatable

Equatable是和比较相关的协议,遵守该协议表示实例能够用于相等的比较,需要重载==运算符。

struct Student: Equatable 
    var math: Int
    var english: Int

// 重载== 运算符
func == (s1: Student, s2: Student) -> Bool 
    return s1.math == s2.math && s1.english == s2.english

Student 遵守 Equatable 并且重载了 == 运算符后就能直接比较两个学生的成绩是否相等了

let s1 = Student(math: 80, english: 60)
let s2 = Student(math: 70, english: 90)
s1 == s1 // false

值得注意的是,由于重载==运算符是遵守Equatable协议后要求我们试下的,因此重载方法应该紧跟在遵守该协议的类型定义后,中间不能有其他代码,否则就报错了。

  • Comparable

Comparable是和比较相关的第二个协议,遵守该协议表示实例能够进行比较,需要重载<运算符。

struct Student: Comparable 
    var math: Int
    var english: Int

// 重载 < 运算符
func < (s1: Student, s2: Student) -> Bool 
    return (s1.math + s1.english) > (s2.math + s2.english)

let s1 = Student(math: 80, english: 60)
let s2 = Student(math: 70, english: 90)
s1 < s2 // true 
  • CustomStringConvertible

CustomStringConvertible 提供了一种用文本表示一个对象或者结构体的方式,可以再任何遵守该协议的类型中自定义表示结构的文本,需要覆盖description属性。

struct Student: CustomStringConvertible 
    var math: Int
    var english: Int
    var description: String 
        return "Your math:" + String(math) + ", english:" + String(english)
    

let s1 = Student(math: 80, english: 60)
print(s1) // Your math: 80, english: 60
  • ExpressibleByArrayLiteral

ExpressibleByArrayLiteral 提供了使用数组文本初始化的类型的能力,具体来说使用逗号分隔的值、实例、字面值列表,方括号以创建数组文本。遵守该协议需要实现`init(arrayLiteral elements: Person.Element…)方法。

struct Person: ExpressibleByArrayLiteral 
    var name: String = ""
    var jon: String = ""
    typealias: Element: String
    init(arrayLiteral Elements: Person.Element...) 
        if elements.count == 2
            name = elements[0]
            job = elements[1]
        
    

let p1: Person = ["jack", "teacher"]
print(p1.name) // jack
print(p1.job) // teacher

上面的代码用到了之前关联类型,通过遵守ExressibleByArrayLiteral,现在Person就可以使用数组直接创建实例了。类似的协议还有ExpressibleByDictionaryLiteral、ExpressibleByStringLiteral、ExpressibleByBooleanLiteral 、ExpressibleByIntegerLiteral等等,相信大家通过名称大概就能菜蔬具体作用了。

为什么要使用协议

  • 协议可以作为类型使用

协议作为一种类型是苹果在Swift中提出的,并且在官方文档中还为我们具体指出了可以将协议当做类型使用的具体场景:

1.在函数、方法或者初始化容器里作为形式参数类型或者返回类型;

2.作为常量、变量或者属性的类型;

3.作为数组、字典或者其他存储器的元素的类型。

  • 协议可以解决面向对象中一些棘手的问题

如图所示的类结构图中麻雀 在宠物类图中的位置就显得比较尴尬,之所以尴尬是因为麻雀作为一种鸟类,应该继承 ,但是如果继承了鸟,就相当于默认了麻雀是一种宠物,这显然是不和逻辑的。解决此类问题的一般方法如下:

咋一看好像解决了这样的问题,但是仔细想由Swift只支持单继承,麻雀没有继承类就无法体现麻雀作为一种鸟拥有的特性和方法(比如飞翔),如果此时出现了一个新的飞机类,虽然飞机宠物之前没有任何联系,但是飞机是有很多共同特性的(比如飞翔),这样的特性该如何体现呢?答案还是新建一个类成为动物飞机的父类。面向对象就是这样一层一层的向上新建父类最终得到一个超级父类。在OC和Swift中是NSObject ,尽管问题得到了解决,但是麻雀飞机之间的共性并没有得到很好的体现。而协议的出现正是为了解决这类问题。

实际上宠物类图中包括动物飞机等类之间的关系就应该是如上图所示的继承关系。使用协议将“宠物”、“飞翔”等关系看作是一种特性,或者从另一个维度描述这种类别,更重要的是使用协议并不会打破原有的类别之间的继承的父子关系。和飞翔相关的代码统一放在Flyable中,需要“飞翔”这种能力遵守该协议;和宠物相关的代码统一放在PetType中,需要成为宠物遵守该协议。这些协议灵活多变结合原有的面向对象类之间固有的继承关系,完美的描述了这个世界。

Swift中高低协议更多的时候是在描述某种属性,是否该将“宠物”设计成一个类或者一个协议,应该根据当前的项目需求。如果你的世界没有麻雀飞机, 那么“宠物”设计成一个类也是未尝不可甚至是非常合理的,这点需要我们仔细思考。

学习使用协议就不得不提通过协议语法延伸出来的面向协议编程范式,苹果提出Swift是一门支持面向协议贬称高低语言,甚至提倡为我们通过协议、结构体代替部分类的使用,可见协议这种语法以及面向协议编程思想在Swift中是多么重要。

参考文献:

Swift Generic Protocols: What Are They Good For?

我从55个Swift标准库协议中学到了什么?

Swift面向协议编程初探

以上是关于Swift之协议的主要内容,如果未能解决你的问题,请参考以下文章

Swift之深入解析协议Protocol的底层原理

iOS开发-Swift进阶之可选类型Optional & Equatable+Comparable协议!

Swift2编程之道:POP+MVVM

Swift 协议

Swift 协议

Swift 协议