源码

首页 » 归档 » 源码 » Swift中协议的简单介绍-ios学习从入门到精通尽在姬长信

Swift中协议的简单介绍-ios学习从入门到精通尽在姬长信

分享最热门的资讯

前言

熟悉objective-c语言的同学们肯定对协议都不陌生,在Swift中苹果将protocol这种语法发扬的更加深入和彻底。Swift中的protocol不仅能定义方法还能定义属性,配合extension扩展的使用还能提供一些方法的默认实现,而且不仅类可以遵循协议,现在的枚举和结构体也能遵循协议了。基于此本文从 1,协议中定义属性和方法2,协议的继承、聚合、关联类型3,协议的扩展4,Swift标准库中常见的协议5,为什么要使用协议 5个方面结合自身的学习经验简单介绍一下这种“加强型protocol的使用,入门级、属于学习总结,希望能给正在学习Swift的小伙伴们一点启发。

协议中定义属性和方法

  • 协议的定义
    协议为方法、属性、以及其他特定的任务需求或功能定义蓝图。协议可被类、结构体、或枚举类型采纳以提供所需功能的具体实现。满足了协议中需求的任意类型都叫做遵循了该协议(引自文档官方文档)。
    Swift中定义一个协议和定义枚举、结构体或者类的格式是相同的,使用protocol关键字:

//定义一个名字为学生协议
protocol Student {
}
  • 这里Student是使用protocol关键字声明的一个协议,和枚举、结构体、类命名原则相似,Student首字母大写表示在以后的使用过程中很可能会将Student看作是一个类型使用。

  • 协议中定义属性

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

protocol Student {
  //定义一个可读可写的 name 属性
  var name: String { get set }
  //定义一个可读的 birthPlace 属性
  var birthPlace: String { get }
  //定义一个类属性 record
  static var qualification: String {get}
}
  • 和定义方法一样,我们只需要确定该属性具体是什么类型并且添加对应的关键字,不需要具体的实现,更不能为他们赋上初始值(类似于计算属性)。定义好属性之后,我们就可以利用属性来做点事情了。

struct Puple: Student {
  static var qualification: String = "小学"
  var name: String
  var birthPlace: String = "北京"
}
var p1 = Puple(name: "小明", birthPlace: "上海")
  • 定义一个Puple结构体遵循Student协议,该结构体中必须存在协议要求声明的三个属性matrikelnummernamebirthPlace,static修饰的类型属性必须被有初始值或者存在getset方法。对于普通的实例属性协议并不关心是计算型属性还是存储型属性。实例中的属性同样可以被修改:

var p1 = Puple(name: "小明", birthPlace: "上海")
Puple.qualification = "中学"

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

var s1: Student = p1
s1.birthPlace = "广州"

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

  • 协议中定义方法

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

protocol Student {
  //类方法
  static func study()
  //实例方法
  func changeName()
}
struct CollageStudent: Student {
  //类方法实现
  static func study() {
  }
  //实例方法实现
  func changeName() {
  }
}
//方法的调用
CollageStudent.study()
var c1 = CollageStudent()
c1.changeName()

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

protocol Student {
  mutating func changeName()
}
struct CollageStudent: Student {
  mutating func changeName() {
      self.name = "小明"
  }
}
var c1 = CollageStudent()
c1.changeName()
  • 协议中的初始化器

我们可以在协议中定义遵循协议的类型需要实现的指定初始化器(构造函数)或者便捷初始化器。

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中并没有规定不同的协议内方法不能重名(这样的规定也是不合理的)。因此我们在自定义多个协议中方法重名的情况是可能出现的,比如有TextOneTextTwo两个协议,定义如下:

protocol TextOne {
  func text() -> Int
}
protocol TextTwo {  
  func text() -> String
}

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

struct Person: TextOne, TextTwo {
  func text() -> Int {
      return 10
  }
  func text() -> String { 
      return "hello"
  }
}
let p1 = Person()
//尝试调用返回值为Int的方法
let num = p1.text()
//尝试调用返回值为String的方法
let string = p1.text()

上面的调用肯定是无法通过的,因为编译器无法知道同名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 {
}

如上所示,任何遵守了ThreeProtocol协议的类型都应该同时实现OneProtocol 和 TwoProtocol的要求必须实现的方法或属性(引自官方文档,概念比较简单)。

  • 协议的聚合

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

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

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

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

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

  • 协议的检查

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

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

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

//只能被类遵守的协议
protocol FourProtocol: class ,ThreeProtocol {
}
//此处报错
struct Person: FourProtocol {
}
class Perple: 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类型,表示3000千克也就是3吨

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

协议的扩展

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

  • 扩展协议的属性和方法

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

protocol Score {
  var math: Int { get set}
  var english: Int {get 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 {
  /// A textual representation of the value.
  public var description: String { get }
}

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

  • 总结

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

Swift标准库中常见的协议

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

2474121-3141e32cfd89e222.png

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 == s2 //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, 

(0)

本文由 姬長信 创作,文章地址:https://blog.isoyu.com/archives/1733.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:12 月 19, 2016 at 05:03 下午

关键词:, , , ,

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复