属性与方法
我们在之前讲过的类和结构体中定义的常量/变量和函数,就是所谓的属性和方法。
首先我们聊聊类中的属性。
类的属性介绍
在Swift的类中,属性分为如下三种:
- 存储属性:用于存储类的实例的常量和变量
- 计算属性:通过某种方式计算出来的属性
- 类属性:与整个类自身相关的属性
一、存储属性
存储属性是最简单的属性,它作为类实例的一部分,用于存储常量和变量。
我们可以给存储属性提供一个默认值,也可以在初始化方法中对其进行初始化。
代码语言:javascript复制class Student {
var name : String?
var age : Int = 19
var chineseScore : Double = 0.0
var mathScore : Double = 0.0
}
//创建实例对象
var student = Student()
//给存储属性赋值
student.name = "norman"
student.age = 18
student.chineseScore = 98.0
student.mathScore = 96.0
本例中的name和age都是存储属性,用来记录该学生的姓名和年龄。
chineseScore和mathScore也是存储属性,用来记录该学生的语文分数和数学分数。
二、计算属性
计算属性并不存储实际的值,而是提供一个getter和一个可选的setter来间接获取和设置其属性值。
计算属性一般只提供getter方法,不提供setter方法。
如果只提供getter方法,而不提供setter方法的话,则该计算属性为只读属性,并且可以省略掉get{}。
代码语言:javascript复制class Student {
//存储属性
var name : String?
var age : Int = 19
var chineseScore : Double = 0.0
var mathScore : Double = 0.0
//计算属性
var averageScore : Double {
get {
return (chineseScore mathScore)/2
}
}
}
//创建实例对象
var student = Student()
//给存储属性赋值
student.name = "norman"
student.age = 18
student.chineseScore = 98.0
student.mathScore = 96.0
//获取计算属性
student.averageScore // 97
本例中,averageScore是计算属性,它的值是由chineseScore和mathScore计算得来。
需要注意的是,averageScore的定义如下:
代码语言:javascript复制 //计算属性
var averageScore : Double {
get {
return (chineseScore mathScore)/2
}
}
可以简化为:
代码语言:javascript复制 //计算属性
var averageScore : Double {
return (chineseScore mathScore)/2
}
三、类属性
类属性是与类相关联的,而不是与类的实例相关联。
所有的类和实例都共有一份类属性,因此在某一处修改之后,该类属性就会被修改。
类属性的设置和修改需要通过类来完成。
下面是类属性的写法:
代码语言:javascript复制class Student {
//存储属性
var name : String?
var age : Int = 19
var chineseScore : Double = 0.0
var mathScore : Double = 0.0
//计算属性
var averageScore : Double {
return (chineseScore mathScore)/2
}
//类属性
static var courseCount : Int = 3
}
//创建实例对象
var student = Student()
//给存储属性赋值
student.name = "norman"
student.age = 18
student.chineseScore = 98.0
student.mathScore = 96.0
//获取计算属性
student.averageScore // 97
//类属性的设值与取值
Student.courseCount = 6
print(Student.courseCount) // 6
四、懒加载属性
懒加载属性是指第一次被调用的时候才会计算其初始值的属性,也就是说,懒加载的对象在第一次使用的时候才会真正被加载到内存中。
在OC中,我们通过gettter方法来实现懒加载。
但是在Swift中,我们是在属性的声明前使用lazy关键字来表示该属性是延迟加载(即懒加载)的。
代码语言:javascript复制class Student {
//懒加载属性
//在第一次使用到该属性的时候,执行闭包,将闭包d额返回值赋值给属性
lazy var terchers: [String] = {
() -> [String] in
return ["tercher1", "tercher2", "tercher3"]
}()
}
//创建实例对象
var student = Student()
print(student.terchers) // ["tercher1", "tercher2", "tercher3"]
总结:
1,存储属性是最先被初始化的
2,存储属性初始化完毕之后会调用类的构造方法,可以在这里对存储属性进行赋值
3,懒加载属性、类属性、全局属性都是在第一次使用的时候初始化一次,以后调用的时候都不再初始化。
4,最后一点需要注意的是,如果某属性需要依赖其他的属性计算得来,那么需要使用的是计算属性,切勿使用懒加载属性。
监听属性的改变
在OC中,我们可以通过setter方法来监听属性的改变。
在Swift中,我们可以通过属性观察者来监听和响应属性值的变化。
但是,我们往往是通过属性观察者来监听存储属性和类属性的变化;对于计算属性,我们可以在它的setter方法中直接观察并响应这种值的变化。
代码语言:javascript复制class Student {
var mathScore : Double = 0.0 {
//属性值即将改变,还没有改变的时候调用
//参数名默认是newValue,支持自定义
willSet{
print("mathScore即将改变,最新值是(newValue)")
}
//属性值改变之后立即调用
//参数名默认是oldValue,支持自定义
didSet{
print("mathScore改变了,旧值是(oldValue)")
}
}
}
//创建实例对象
var student = Student()
//在赋值的时候监听属性的改变
student.mathScore = 98.0
打印结果为:
mathScore即将改变,最新值是98.0
mathScore改变了,旧值是0.0
定义观察者:
- willSet在属性值被存储之前设置。此时新属性值作为一个常量参数被传入,该参数名默认为newValue,可以自定义。
- didSet在新属性值被存储之后立即调用。此时旧属性值作为一个常量参数被传入,该参数名默认为oldValue,可以自定义。
需要注意的是,willSet和didSet只有在属性值改变的时候才会被调用,在初始化的时候不会去调用这些监听方法。
属性的继承与重写
属性的继承:
子类可以继承父类的属性,包括存储属性、计算属性和类属性,还可以继承父类的属性观察器。
属性的重写:
- 无论继承的是存储属性还是计算属性,子类都可以通过提供getter和setter对属性进行重写
- 可以将一个继承的属性重写为一个读写属性
- 不可以将一个继承来的读写属性重写为只读属性
- 如果重写时提供了setter方法,那么一定要提供getter方法
属性观察器的重写:
- 无论父类有没有为属性添加属性观察器,子类都可以添加属性观察器
- 如果父类已经添加了属性观察器,当属性发生变化时,父类与子类都会得到通知。
- 属性观察器只能用于存储属性,不能用于计算属性。计算属性在setter方法里就可以监听到属性的变化。
实例方法
所谓实例方法,指的是类实例、结构体实例,或者枚举实例的函数。
这里的方法其实就是函数,只不过放在类、结构体或者枚举中时称之为方法。
self属性
每一个类的实例都隐含一个名为self的属性,这个self指的是调用当前方法或者属性的实例对象,我们可以使用self来访问当前类中的属性和方法。
代码语言:javascript复制class Student {
//定义一个实例方法
func say(info : String) {
print("我想吃(info)")
}
func eat(food : String) {
//self指向的是当前类的实例
self.say(info: food)
print("给你吃(food)")
}
}
//创建实例对象
var student = Student()
student.eat(food: "?")
执行打印结果为:
我想吃?
给你吃?
值类型在实例方法中修改属性和调用方法
值类型(比如结构体)默认情况下不能在实例方法中修改属性,但是我们可以在函数前面放一个mutating关键字来实现。
代码语言:javascript复制struct Student {
var food = "饺子"
//定义一个实例方法
func changeFood(newfood : String) {
self.food = newfood // 报错 Cannot assign to property: 'self' is immutable
print("食物变为(self.food)")
}
func eat(food : String) {
//self指向的是当前类的实例
self.changeFood(newfood: food) // 报错Cannot use mutating member on immutable value: 'self' is immutable
print("给你吃(self.food)")
}
}
上面代码执行会报错,改为如下:
代码语言:javascript复制struct Student {
var food = "饺子"
//定义一个实例方法
mutating func changeFood(newfood : String) {
self.food = newfood
print("食物变为(self.food)")
}
mutating func eat(food : String) {
//self指向的是当前类的实例
self.changeFood(newfood: food)
print("给你吃(self.food)")
}
}
//创建实例对象
var student = Student()
student.eat(food: "?")
执行结果为:
代码语言:javascript复制食物变为?
给你吃?
类型方法
类型方法是与实例方法相对应的。实例方法由类的实例对象来调用,而类型方法是由类本身来调用。
类型方法又分为如下两种:
- 静态方法:在函数前使用static关键字修饰,能够在类、结构体中使用。
- 类方法:在函数前使用class关键字修饰,只能在类中使用
class Student {
var food = "饺子"
//类方法
class func play() {
print("玩的开心")
}
//静态方法
static func learn() {
print("学得有趣")
}
}
Student.play() // 玩的开心
Student.learn() // 学得有趣
static和class两个关键字的异同点
相同点:
- 二者都可以用于修饰方法。static修饰方法的叫做静态方法,class修饰的方法叫做类方法
- 二者都可以用于修饰计算属性
不同点:
- class不能用于修饰存储属性,而static可以修饰存储属性。static修饰的存储属性叫做静态变量/常量
- class修饰的计算属性可以被重写,static修饰的计算属性不能被重写
- class修饰的类型方法可以被重写,static修饰的类型方法不能被重写
- class修饰的类方法被重写时,可以使用static让方法变为静态方法
- class修饰的计算属性被重写的时候,可以使用static令其变为静态属性,这样在它的子类中就不能被重写了。
- class关键字只能是在类中使用,而static关键字可以在类中、结构体中或者枚举中使用。
构造函数与析构函数
所谓构造函数,就是在构造对象的时候调用的函数。
所谓析构函数,就是在销毁对象的时候调用的函数。
构造函数的介绍
构造函数用于初始化一个类的实例(创建对象)。
在创建一个类的对象的时候,必然会调用构造函数。即便是没有手动编写任何的构造函数,编译器也会提供一个默认的构造函数。
默认构造函数
在创建类或者结构体的实例的时候,必须为所有的存储属性设置一个初始值,如果不在定义的时候初始化值,那么就要在构造函数中赋初始化值。
构造函数使用init关键字来写:
代码语言:javascript复制class Student {
var food : String
//默认构造函数
init() {
//初始化存储变量的值
food = "饺子"
}
}
一定要注意:一个类中定义的存储属性,要么定义成可选型,要么在定义的时候赋初始值,要么在构造函数中初始化,否则就会编译报错。
另外还需要注意的一点是,默认构造函数是没有func关键词修饰的。并且默认构造函数无需手动调用,编译器会在创建对象的时候自动调用。
自定义构造函数
如果我们想要在创建对象的时候手动传入存储属性的初始值,那么就需要自定义一个成员构造函数。
自定义构造函数和默认构造函数是可以同时存在的。
代码语言:javascript复制class Person {
var name : String
var age : Int
var gender : String
//默认构造函数
init() {
name = "norman"
age = 18
gender = "male"
}
//自定义的成员构造函数
init(name : String, age : Int, gender : String) {
self.name = name
self.age = age
self.gender = gender
}
}
var person1 = Person() // 默认构造函数
var person2 = Person(name: "lavie", age: 19, gender: "男") // 自定义构造函数
需要注意的一点是,自定义成员构造函数的函数名不能瞎写,必须是init。
默认构造函数和自定义成员构造函数的函数名都是init,二者的不同点在于:
默认构造函数没有参数,而自定义的成员构造函数是有参数的。
还有一点需要注意的是,前面我们讲过的结构体,它的构造函数也是分为两种,一个是不需要传值的默认构造函数,一个是需要传值的成员构造函数。但是结构体的成员构造函数是编译器默认提供的,不需要程序员手动去构建;而类的成员构造函数需要我们手动去自定义构建。
值类型的构造函数委托
构造函数可以调用其它的构造函数来执行部分实例的初始化,这个过程就是所谓的构造函数委托。
需要注意的是,前面我们也提到,结构体的成员构造函数是编译器默认提供的,不需要程序员手动去构建。此时我们在创建结构体实例的时候既可以使用默认构造函数来创建,也可以使用成员构造函数来实现。
但是如果我们手动在结构体中写了构造函数,那么一定要同时写默认构造函数和成员构造函数,如果只写其一的话,那么在创建实例的时候就只能通过写的那个构造函数来创建。
代码语言:javascript复制struct Size {
var width : Double = 0.0
var height : Double = 0.0
//默认构造函数
init() {
//构造函数委托
self.init(width : 10.0, height : 10.0)
}
//成员构造函数
init(width : Double, height : Double) {
self.width = width
self.height = height
}
}
var size1 = Size() // 如果只写成员构造函数,那么就不能通过该方式创建实例
var size2 = Size(width: 18, height: 83) // 如果只写默认构造函数,那么就不能通过该方式创建实例
//这里由于同时写了默认构造函数和成员构造函数,所以可以通过两种方式创建实例
//如果既没有手写默认构造函数,也没有手写成员构造函数,那么编译器会默认添加这两种构造函数,所以也可以通过两种方式创建实例
由于值类型(结构体、枚举)不支持继承,所以他们的构造函数委托相对比较简单。接下来我们看看类的构造函数委托。
类类型的构造函数委托
首先重述一个观点,类的所有存储属性,包括从父类继承来的存储属性,都必须在初始化期间分配初始值。
Swift为类类型定义了两种构造函数以确保所有的存储属性接收一个初始值,他们就是指定构造函数(Designated Initializer)和便捷构造函数(Convenient Initializer)。
指定构造函数是类的主要构造函数。指定构造函数用于初始化类的所有存储属性。一个类通常只有一个指定构造函数,并且至少有一个指定构造函数。
便捷构造函数是次要的。可以在相同的类里面定义一个便捷构造函数,然后在便捷构造函数中调用指定构造函数来给指定构造函数设置默认形式参数。一定要注意的是,便捷构造函数只调用其他的构造函数(指定或者便捷),除此之外不做其他的事情。
Swift中,类类型的构造函数委托有如下三个规则:
1,在某类的指定构造函数的最后,必须调用其父类的指定构造函数
2,一个类的便捷构造函数,必须调用该类里面的另外一个构造函数(可以是指定,也可以是便捷)
3,一个类的便捷构造函数,最终总会调到该类的指定构造函数。
总结一下就是说:
指定构造函数必须总是向上委托;便捷构造函数必须总是横向委托。
如下图所示:
代码语言:javascript复制class Person {
var name : String
//父类的指定构造函数
init(name : String) {
self.name = name
}
}
class Student : Person {
var age : Int
//子类的指定构造函数
init(age : Int) {
//初始化子类中的属性
self.age = age
//一定要在子类的属性初始化完成之后,再调用父类的构造函数
super.init(name: "Norman")
}
//子类的便捷构造函数
convenience init() {
self.init(age : 18)
}
}
析构函数
与OC一样,Swift也是采用ARC来管理对象的内存。
当引用计数是0的时候,系统会自动调用析构函数(OC中是dealloc,Swift中是deinit),不可以手动调用。
代码语言:javascript复制 //析构函数
deinit {
//执行析构过程
}
协议
协议的定义
协议的定义方式与类、结构体、枚举的定义方式非常相似:
代码语言:javascript复制protocol SomeProtocol {
//属性
//方法
}
协议中的属性需要遵循以下几点:
- 必须设置为变量var
- 不可以有默认值
- 必须设置是{get}还是{get set},{get}表示只读,{get set}表示可读可写
协议中的方法需要注意以下几点:
- 可以定义普通方法,也可以是mutating方法
- 方法不能有方法体
- 方法中的参数不能有默认值
protocol SomeProtocol {
//属性
var name : String {get set}
//方法
func play(company : String)
mutating func eat()
}
协议的遵循
遵循协议的写法跟继承父类的写法其实是一样的:
代码语言:javascript复制//协议
protocol SomeProtocol {
//属性
var name : String {get set}
//方法
func play(company : String)
mutating func eat()
}
//父类
class Person {
var age : Int = 18
}
//遵循协议,继承父类
class Student : Person, SomeProtocol {
//实现协议中的属性
var name: String = ""
//实现协议中的方法
func play(company: String) {
print("play with (company)")
}
func eat() {
print("to dine")
}
}
实现协议中的属性的时候需要注意以下几点:
- 此时属性可以设置默认值
- 如果协议中属性是可读可写的,那么实现协议的时候需要声明为var类型
- 如果协议中的属性是只读的,那么实现协议的时候既可以声明为var,也可以声明为let
实现协议中的方法的时候,我们可以为方法中的参数设置默认值。
协议的继承
代码语言:javascript复制//父协议
protocol ParentProtocol {
//属性
var name : String {get set}
//方法
func play(company : String)
mutating func eat()
}
//子协议
protocol SonProtocol : ParentProtocol {
func study()
}
//遵循协议
class Student : SonProtocol {
//实现父协议中的内容
var name: String = ""
func play(company: String) {
print("play with (company)")
}
func eat() {
print("to dine")
}
//实现子协议中的内容
func study() {
print("study")
}
}
协议中方法的可选
1,optional关键字
代码语言:javascript复制@objc protocol ParentProtocol {
func play(company : String)
//该方法可选
@objc optional func eat()
}
//遵循协议
class Student : ParentProtocol {
//实现协议中的内容
//此时可以不实现协议中的可选方法,当然也可以实现
func play(company: String) {
print("play with (company)")
}
}
2,扩展协议
代码语言:javascript复制protocol ParentProtocol {
func play(company : String)
func eat()
}
extension ParentProtocol {
//可以在协议的拓展中对协议中的方法进行默认的实现
func eat() {
print("eat")
}
}
//遵循协议
class Student : ParentProtocol {
//实现协议中的内容
//如果协议中的方法已经在拓展中默认实现了,这里可以实现也可以不实现
func play(company: String) {
print("play with (company)")
}
}
我们可以利用协议的扩展,对协议中的一些方法进行默认实现。如果在协议的扩展中对某些方法进行了实现,那么在遵循协议的类里面,可以不实现已经有了默认实现的方法。
协议的运用——代理模式
代码语言:javascript复制//第1步,定义一个协议,规定需要完成的任务
protocol BuyTicketProtocol {
func buyTicket()
}
//第2步,让具体的类或者结构体来实现协议,将任务具体化
class Assistant : BuyTicketProtocol {
func buyTicket() {
print("秘书去买票")
}
}
class Scalpers : BuyTicketProtocol {
func buyTicket() {
print("黄牛去买票")
}
}
//第3步,委托方的一个配置
class Person {
//3.1,首先设置代理属性
var delegate : BuyTicketProtocol?
func goToShanghai() {
print("准备去上海,找个人去买票")
//3.2,在需要完成任务的时候通过代理来进行操作
delegate?.buyTicket()
print("买到票了,准备出发")
}
}
//第4步,设置委托方的代理
var person = Person()
person.delegate = Scalpers() // 设置委托方的代理
person.goToShanghai()
打印结果为:
准备去上海,找个人去买票
黄牛去买票
买到票了,准备出发
扩展
扩展可以为类、结构体、枚举、协议添加新的功能。Swift中的扩展类似于OC中的分类。
扩展可以做的事情:
- 添加计算属性
- 定义方法
- 提供新的构造函数
- 使现有类型遵循某种协议
在Swift中,使用extension关键字来实现扩展。
其语法如下:
代码语言:javascript复制extension SomeType {
// 这里的SomeType可以是类、枚举、结构体,也可以是协议
// 在这里面去添加一些新的内容
}
扩展计算属性
扩展可以在不动原有类型的情况下,向已有的类型添加计算实例属性和计算类型属性。
代码语言:javascript复制extension Int {
//给Int类型增加一个平方属性
var square: Int {
return self * self
}
}
var a = 2
print(a.square) // 4
Int是结构体,上例演示了通过扩展给一个结构体增加计算实例属性。
扩展构造函数
扩展还可以向已有类型添加新的初始化器。
代码语言:javascript复制extension CGRect {
//自定义一个构造函数
init(center : CGPoint, size : CGSize) {
let originX = center.x - size.width/2
let originY = center.y - size.height/2
self.init(x: originX, y: originY, width: size.width, height: size.height)
}
}
let centerRect = CGRect(center: CGPoint(x: 10, y: 10), size: CGSize(width: 10, height: 10))
扩展方法
扩展一个普通方法
代码语言:javascript复制extension Int {
func square() -> Int {
return self * self
}
}
var a = 8
print(a.square()) // 64
也可以扩展一个mutating方法。如果你要在某方法里修改值类型的某个属性值,那么就必须在该方法前面加上mutating 关键字:
代码语言:javascript复制extension Int {
mutating func square() {
self *= self
}
}
var a = 8
print(a) // 8
a.square()
print(a) // 64
使现有类型遵循某协议
扩展可以使既有类型遵循一个或者多个协议,协议的遵循书写方式与原类中完全一致。语法如下:
代码语言:javascript复制extension SomeClass : SomeProtocol, AnotherProtocol {
//可以用于分离原类中的代码
}
这个写法在开发中是非常有用的,比如可以将tableView、collectionView的代理从原控制器中给抽离出来,避免臃肿。
面向协议编程
众所周知,Swift是一门面向协议编程的语言。我所理解的面向协议编程是这样的:针对某个需要实现的功能,可以使用协议定义出接口,然后利用协议的拓展为其提供默认的实现。如果在某地方需要这个功能,则只需要声明遵循该协议即可。遵守某个协议的对象调用协议中声明的方法时,如果遵循者本省没有提供方法的实现,那么协议扩展提供的默认实现将会被调用。
代码语言:javascript复制//定义一个协议
protocol WatchMovieProtocol {
func watchAMovie(name : String)
}
//遵循协议并实现协议中的方法
class Student : WatchMovieProtocol {
func watchAMovie(name: String) {
print("去电影院看《(name)》")
}
}
上例中是一个遵循者遵循该协议,如果此时有100个遵循者都需要遵循该协议,并且该协议方法的实现都是一样的,那么是否需要在每一个遵循者里面都实现一下协议方法呢?
当然不需要,我们可以使用协议的拓展为协议中的方法提供一个默认实现:
代码语言:javascript复制//定义一个协议
protocol WatchMovieProtocol {
func watchAMovie(name : String)
}
//利用协议的拓展,给协议中的方法提供默认实现
extension WatchMovieProtocol {
func watchAMovie(name : String) {
print("去电影院看《(name)》")
}
}
//遵循协议
class Student : WatchMovieProtocol {
}
var studet = Student()
studet.watchAMovie(name: "燃情岁月")
打印如下:
去电影院看《燃情岁月》
如果某一个遵循者需要独特的实现,那么我们自己再实现一个协议即可:
代码语言:javascript复制//定义一个协议
protocol WatchMovieProtocol {
func watchAMovie(name : String)
}
//利用协议的拓展,给协议中的方法提供默认实现
extension WatchMovieProtocol {
func watchAMovie(name : String) {
print("去电影院看《(name)》")
}
}
//遵循协议,并且自定义协议方法的实现
class Student : WatchMovieProtocol {
func watchAMovie(name : String) {
print("学生去电影院看《(name)》")
}
}
var studet = Student()
studet.watchAMovie(name: "燃情岁月")
打印如下:
学生去电影院看《燃情岁月》
泛型
所谓泛型,顾名思义,就是广泛类型。也就是说,一开始不确定是什么类型,等到真正使用的时候,根据赋值的数据类型来确定类型。
先来看个案例,比如现在我们需要交换两个Int类型的变量的值,实现如下:
代码语言:javascript复制//交换传入的两个数的值
func SwapTwoInts(a : inout Int, b : inout Int) {
let temp = a
a = b
b = temp
}
var lilyAge = 20
var normanAge = 26
SwapTwoInts(a: &lilyAge, b: &normanAge)
print(lilyAge) // 26
print(normanAge) // 20
我现在又增加了一个需求,就是既需要转换Int类型的两个数值,也需要转换Double类型的两个数值,或者是转换其他类型的两个数值,那怎么办呢?,这些方法仅仅是参数的类型不同,是不是针对每一个类型都需要写一个转换方法呢?
此时就需要用到泛型了。
代码语言:javascript复制//泛型函数
func SwapTwoValues<T>(a : inout T, b : inout T) {
let temp = a
a = b
b = temp
}
var lilyAge = 20
var normanAge = 26
SwapTwoValues(a: &lilyAge, b: &normanAge)
print(lilyAge) // 26
print(normanAge) // 20
var lilyName = "lily"
var normanName = "norman"
SwapTwoValues(a: &lilyName, b: &normanName)
print(lilyName) // norman
print(normanName) // lily
该例中的 T 是一个类型参数(它可以是任意名称,我们一般使用 T 来表示),类型参数用于指定并命名一个占位符类型,并使用<>包裹,放在函数名后边,比如该例中的<T>。可以使用它来指定参数类型或者返回值的类型。在真正调用的时候会被实际的类型替代,如传递的是Int,就替换为Int;如果传递的是Double,就替换为Double等等。
泛型的类型约束
有时候我们需要给泛型做一些约束,比如必须继承自某个父类,或者必须遵循某些协议等,这也是可以做到的。语法如下:
代码语言:javascript复制func someFunction<T : SomeClass, U : SomeProtocol>(someT : T, someU : U) {
}
协议里面的泛型——关联类型
上面所说的是在类或者结构体、枚举中使用泛型,在协议中是不可以这样使用的。协议中使用泛型,是通过 associatedtype 关键字。
代码语言:javascript复制protocol SomeProtocol {
associatedtype Element
func play(parameter1 : Element, parameter2 : Element)
}
struct SomeStruct : SomeProtocol {
func play(parameter1: String, parameter2: String) {
print("play, (parameter1), (parameter2)")
}
}
var s = SomeStruct()
s.play(parameter1: "666", parameter2: "norman")
说明如下:
- 协议中的泛型,是通过关键字 associatedtype 来实现的
- 与类、结构体或者枚举中的泛型一样,协议中的泛型也可以进行类型约束(通过继承或者遵循协议的方式)
异常
如何来描述一个异常
在Swift中,任何一个遵从 Error 协议的类型,都可以用来描述错误。Error 是一个空的protocol,他唯一的功能,就是告诉Swift编译器,遵循该协议的类型用于表示一个错误。
通常,我们使用enum枚举来定义各种错误的可能性。
异常处理
假如我们想要读取文件中的内容,在读取的过程中可能会出错。比如当我们调用方法获取结果为nil的时候,我们并不知道到底发生了什么错误而导致没有获取到结果。
代码语言:javascript复制func readFileContent(filePath : String) -> String? {
//1,路径为空字符串
if filePath == "" {
return nil
}
//2,路径有值,但是该路径下没有对应的文件
if filePath != "/user/desktop/123.plist" {
return nil
}
//3,正确获取到文件内容
return "123"
}
readFileContent(filePath: "1234567")
接下来使用异常对上面方法进行改造:
代码语言:javascript复制// 定义异常
enum FileReadError : Error {
case FileIsNull // 路径为空
case FileNotFound // 路径下对应的文件不存在
}
// 改进方法,让方法抛出异常
func readFileContent(filePath : String) throws -> String {
//1,路径为空字符串
if filePath == "" {
throw FileReadError.FileIsNull
}
//2,路径有值,但是该路径下没有对应的文件
if filePath != "/user/desktop/123.plist" {
throw FileReadError.FileNotFound
}
//3,正确获取到文件内容
return "123"
}
这里需要注意三点:
- 函数的参数与返回值之间要加上throws
- 抛出异常使用 throw 关键字
- 函数的返回值不可以是可选型
接下来我们来看看如何去处理异常。
处理异常有三种方式:
1,do-try-catch 方式,这样可以自己手动处理异常,推荐使用。
代码语言:javascript复制var result = "原始值"
do {
result = try readFileContent(filePath: "123")
} catch {
//有一个隐含参数error
print(error) // FileNotFound
}
print(result) // 原始值
2,try? 方式,该方式不处理异常,如果出现了异常,则会返回一个nil;没有出现异常则正常返回。
代码语言:javascript复制// 最终返回结果为一个可选类型
let result = try? readFileContent(filePath: "")
3,try!方式,当你百分百确保不会出现异常的时候,会使用该方式。因为一旦出现异常,则会立马崩溃。
代码语言:javascript复制//正常取值,不会出现异常
let result = try! readFileContent(filePath: "/user/desktop/123.plist")
print(result) // 123
//出现异常,程序报错
let result = try! readFileContent(filePath: "")
报错信息如下:
defer关键字
defer中的语句的执行时机是:do中语句全部执行完毕后,catch执行之前。
可以使用defer来进行扫尾工作(比如释放资源等),因为不管有没有异常,最终都会走到defer。
代码语言:javascript复制var result = "初始值"
do {
defer {
print("在这里进行扫尾工作")
}
print("try之前")
result = try readFileContent(filePath: "/user/desktop/123.plist")
print("try之后")
} catch {
print(error)
}
print(result)
打印信息如下:
try之前
try之后
在这里进行扫尾工作
123
代码语言:javascript复制var result = "初始值"
do {
defer {
print("在这里进行扫尾工作")
}
print("try之前")
result = try readFileContent(filePath: "")
print("try之后")
} catch {
print(error)
}
print(result)
打印信息如下:
try之前
在这里进行扫尾工作
FileIsNull
初始值