一. 错误
- 错误类型
开发中常见的错误:
语法错误(编译时会报错)
逻辑错误
运行时错误(可能会导致闪退,一般也叫做异常)
- 自定义错误
Swift中可以通过遵守Error协议自定义运行时的错误信息,无论是枚举、结构体、类只要遵守Error协议都可以当做错误信息,Error协议源码:
public protocol Error {
}
enum SomeError : Error {
case illegalArg(String)
case outOfBounds(Int, Int)
case outOfMemory
}
函数内部通过throw抛出自定义Error,可能会抛出Error的函数必须加上throws声明
func divide(_ num1: Int, _ num2: Int) throws -> Int {
if num2 == 0 {
throw SomeError.illegalArg("0不能作为除数")
}
return num1 / num2
}
需要使用try调用可能会抛出Error的函数
var result = try divide(20, 10)
二. 错误处理
处理Error有两种方式:
1. 通过do-catch捕捉Error
func test() {
print("1")
do {
print("2")
print(try divide(20, 0))
print("3")
} catch let SomeError.illegalArg(msg) {
print("参数异常:", msg)
} catch let SomeError.outOfBounds(size, index) {
print("下标越界:", "size=\(size)", "index=\(index)")
} catch SomeError.outOfMemory {
print("内存溢出")
} catch {
print("其他错误") }
print("4")
}
test()
// 1
// 2
// 参数异常: 0不能作为除数
// 4
没有Error情况下,上面打印是“1,2,3,4”,抛出Error后,try下一句直到作用域结束的代码都将停止运行,通过上面的打印也可以验证。
下面代码也可以捕获所有error,然后用switch区分是哪个error,道理都是一样的,如下:
do {
try divide(20, 0)
} catch let error { //可以把let error省略,因为catch后默认就有⼀个error给你使⽤
switch error {
case let SomeError.illegalArg(msg):
print("参数错误:", msg)
default:
print("其他错误")
}
}
可以把let error省略,因为catch后默认就有⼀个error给你使⽤,如下:
do {
try divide(20, 0)
} catch is SomeError {
print("SomeError")
}
2. 不捕捉Error,在当前函数增加throws声明,Error将自动抛给上层函数
如果最顶层函数(main函数)依然没有捕捉Error,那么程序将终止
func test() throws {
print("1")
print(try divide(20, 0))
print("2")
}
try test()
// 1
// Fatal error: Error raised at top level
补充:如果test1调用test2,test2调用divide,divide会抛出异常,按理说要求test2要把所有错误处理完,如果test2不处理错误或者只处理一部分错误,都要使用throws把错误往上抛出去,如果test2处理完所有错误,就不需要throws了,test1同理。
3. 一些关键字:
- try?、try!
可以使用try?、try!调用可能会抛出Error的函数,这样就不用去处理Error。
try?、try!会自动处理,对于try?,如果抛出错误就返回nil,最后结果是可选类型,对于try!自动处理之后会隐式解包。
func test() {
print("1")
var result1 = try? divide(20, 10) // Optional(2), Int?
var result2 = try? divide(20, 0) // nil
var result3 = try! divide(20, 10) // 2, 隐式解包成Int
print("2")
}
test()
下面a、b是等价的
var a = try? divide(20, 0)
var b: Int?
do {
b = try divide(20, 0)
} catch { b = nil }
- rethrows
rethrows和throws用法完全一样,只不过声明的含义不一样
rethrows表明:函数本身不会抛出错误,但调用闭包参数抛出错误,那么它会将错误向上抛
func exec(_ fn: (Int, Int) throws -> Int, _ num1: Int, _ num2: Int) rethrows {
print(try fn(num1, num2))
}
// Fatal error: Error raised at top level
try exec(divide, 20, 0)
- defer
defer语句:用来定义以任何方式(抛错误、return等)离开代码块前必须要执行的代码
defer语句将延迟至当前作用域结束之前执行
举例:比如文件操作的时候,最后必须要关闭文件
func open(_ filename: String) -> Int {
print("open")
return 0
}
func close(_ file: Int) {
print("close")
}
func processFile(_ filename: String) throws {
let file = open(filename)
defer {
close(file)
}
// 使用file
// ....
try divide(20, 0)
// close将会在这里调用
}
try processFile("test.txt")
// open
// close
// Fatal error: Error raised at top level
如果上面的 try divide(20, 0) 操作抛出了错误,那么后面的关闭文件的代码就不会执行了,所以要使用defer语句,把关闭文件的代码写在defer语句里面,保证无论如何最后都会执行。
defer语句的执行顺序与定义顺序相反,如下:
func fn1() { print("fn1") }
func fn2() { print("fn2") }
func test() {
defer { fn1() }
defer { fn2() }
}
test()
// fn2
// fn1
- assert(断言)
很多编程语言都有断言机制:不符合指定条件就抛出运行时错误
因为自定义抛出的错误可以捕捉,但是断言不能捕捉,所以断言常用于调试(Debug)阶段的条件判断
默认情况下,Swift的断言只会在Debug模式下生效,Release模式下会忽略
func divide(_ v1: Int, _ v2: Int) -> Int {
assert(v2 != 0, "除数不能为0")
return v1 / v2
}
print(divide(20, 0))
// Assertion failed: 除数不能为0: file MyPlayground.playground, line 2
增加Swift Flags,修改断言的默认行为,如下图:
-assert-config Release:强制关闭断言
-assert-config Debug:强制开启断言
- fatalError
如果遇到严重问题希望结束程序运行,可以直接使用fatalError函数抛出错误(这也是无法通过do-catch捕捉的错误)
使用了fatalError函数,就不需要再写return
func test(_ num: Int) -> Int {
if num >= 0 {
return 1
}
fatalError("num不能小于0")
}
在某些不得不实现、但不希望别人调用的方法,可以考虑内部使用fatalError函数
class Person { required init() {} }
class Student : Person {
required init() { fatalError("don't call Student.init") }
init(score: Int) {}
}
var stu1 = Student(score: 98)
var stu2 = Student()
// Fatal error: don't call Student.init: file MyPlayground.playground, line 3
- 局部作用域
Swift中不能使用{},但是可以使用 do {} 实现局部作用域
do {
let dog1 = Dog()
dog1.age = 10
dog1.run()
}
do {
let dog2 = Dog()
dog2.age = 10
dog2.run()
}
二. 泛型
1. 泛型(Generics)
泛型可以将类型参数化,提高代码复用率,减少代码量
Swift为了安全性,泛型在使用的时候要确定类型(要么自动推导出类型,要么指定类型)
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var i1 = 10
var i2 = 20
swapValues(&i1, &i2) //自动识别为Int
var d1 = 10.0
var d2 = 20.0
swapValues(&d1, &d2) //自动识别为Double
struct Date {
var year = 0, month = 0, day = 0
}
var dd1 = Date(year: 2011, month: 9, day: 10)
var dd2 = Date(year: 2012, month: 10, day: 11)
swapValues(&dd1, &dd2) //自动识别为Date
泛型函数赋值给变量,要在:后面明确类型,如下:
func test<T1, T2>(_ t1: T1, _ t2: T2) {}
var fn: (Int, Double) -> () = test
2. 泛型使用举例
//进栈出栈操作:
class Stack<E> {
var elements = [E]() //泛型数组初始化器
func push(_ element: E) { elements.append(element) } //添加谁就返回谁
func pop() -> E { elements.removeLast() } //删除谁就返回谁
func top() -> E { elements.last! } //最后一个元素是谁就返回谁
func size() -> Int { elements.count }
}
var stack = Stack<Int>() //泛型Int
stack.push(11)
stack.push(22)
stack.push(33)
print(stack.top()) // 33
print(stack.pop()) // 33
print(stack.pop()) // 22
print(stack.pop()) // 11
print(stack.size()) // 0
//如果是继承,也需要加上泛型类型
class SubStack<E> : Stack<E> {}
//如果定义成结构体,需要加上mutating
struct Stack<E> {
var elements = [E]()
mutating func push(_ element: E) { elements.append(element) }
mutating func pop() -> E { elements.removeLast() }
func top() -> E { elements.last! }
func size() -> Int { elements.count }
}
//使用泛型的枚举:
enum Score<T> {
case point(T)
case grade(String)
}
let score0 = Score<Int>.point(100)
let score1 = Score.point(99)
let score2 = Score.point(99.5)
let score3 = Score<Int>.grade("A") //就算没有用到泛型,也要把泛型类型写上
3. 泛型的本质
泛型内部是一个函数还是根据传入不同的类型生成不同的函数呢?
如下代码,如果下面两个函数地址一样说明是一个函数,如果两个函数地址不一样说明生成了两个不同的函数
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var i1 = 10
var i2 = 20
swapValues(&i1, &i2) //断点
var d1 = 10.0
var d2 = 20.0
swapValues(&d1, &d2) //断点
打断点,查看汇编,发现两个函数地址一样,说明还是一个函数,没有根据类型不同生成不同的函数。
那么是怎么做到共用一个函数解决问题的呢?
通过查看汇编可以发现,是把泛型类型的元信息传进去了,这样swapValues函数内部就会做相应的处理。
4. 关联类型(Associated Type)
关联类型的作用:给协议中用到的类型定义一个占位名称
协议中可以拥有多个关联类型
protocol Stackable {
associatedtype Element //协议中使用泛型,用关联类型
mutating func push(_ element: Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}
//如果是String类型
class StringStack : Stackable {
// 给关联类型设定真实类型,下面可以推导出来就可以省略
// typealias Element = String
var elements = [String]()
func push(_ element: String) { elements.append(element) }
func pop() -> String { elements.removeLast() }
func top() -> String { elements.last! }
func size() -> Int { elements.count }
}
var ss = StringStack()
ss.push("Jack")
ss.push("Rose")
//如果写成泛型类型,可以提高代码复用率
class Stack<E> : Stackable {
// typealias Element = E
var elements = [E]()
func push(_ element: E) { elements.append(element) }
func pop() -> E { elements.removeLast() }
func top() -> E { elements.last! }
func size() -> Int { elements.count }
}
var ss = Stack<String>()
ss.push("Jack")
ss.push("Rose")
5. 泛型类型约束
要求传入的泛型,必须是Person类或者其子类并且遵守Runnable协议
protocol Runnable { }
class Person { }
func swapValues<T : Person & Runnable>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
更复杂的泛型类型约束,如下:
//协议里面的泛型,要求遵守Equatable协议
protocol Stackable {
associatedtype Element: Equatable
}
//写成泛型,并遵守Equatable协议
class Stack<E : Equatable> : Stackable { typealias Element = E }
//要求,S1、S2都要遵守Stackable协议,并且S1、S2的关联类型相等,并且S1的关联类型遵守Hashable协议
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2) -> Bool
where S1.Element == S2.Element, S1.Element : Hashable {
return false
}
var stack1 = Stack<Int>()
var stack2 = Stack<String>()
//error: requires the types 'Int' and 'String' be equivalent
//报错是因为S1的关联类型是Int,S2的关联类型是String,关联类型不相等
equal(stack1, stack2)
6. 协议类型的注意点
返回Runnable协议,就是返回遵守Runnable协议的东西
protocol Runnable {}
class Person : Runnable {}
class Car : Runnable {}
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}
var r1 = get(0)
var r2 = get(1)
如果上面协议中有关联类型,如下:
protocol Runnable {
associatedtype Speed
var speed: Speed { get }
}
class Person : Runnable {
var speed: Double { 0.0 }
}
class Car : Runnable {
var speed: Int { 0 }
}
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}
var r1 = get(0)
var r2 = get(1)
编译上面的代码,会报错如下,这是因为编译器在编译完毕的时候还不确定r1、r2协议里面泛型的关联类型是什么,只有在运行过程中才会知道泛型的关联类型(r1是Double,r2是Int)
1.png解决方案①:使用泛型
func get<T : Runnable>(_ type: Int) -> T { //返回一个遵守Runnable协议的东西
if type == 0 {
return Person() as! T
}
return Car() as! T
}
var r1: Person = get(0) //指定返回值是Person类型,Person类型遵守了Runnable,所以这样不报错
var r2: Car = get(1)
解决方案②:使用some关键字声明一个不透明类型
func get(_ type: Int) -> some Runnable { Car() }
var r1 = get(0)
var r2 = get(1)
some限制只能返回一种类型,既然只能返回一种类型,那么编译器肯定知道关联类型是什么了,所以上面那样写不会报错。如果返回两种类型会报错,如下:
3.png用处:我只想让外界知道我返回的是⼀个遵守这个协议的类型,但是真实是什么类型我不告诉你,你只能拿到我返回给你的类型调⽤协议⾥⾯的东⻄,这也是叫不透明类型的原因。
some除了用在返回值类型上,一般还可以用在属性类型上
protocol Runnable { associatedtype Speed }
class Dog : Runnable { typealias Speed = Double }
class Person {
var pet: some Runnable {
return Dog()
}
}
var person = Person()
var pet = person.pet
如上代码,Runnable协议里面有个关联类型,Person想养一个宠物,这个宠物要求要遵守Runnable协议,但是具体养什么宠物不想让你知道,这时候就可以用some,然后返回Dog(),这时候外界只知道你的宠物遵守Runnable协议,并不知道具体是什么宠物。
网友评论