iOS-Swift-错误处理、泛型

一. 错误处理

开发中常见的错误:

  • 语法错误(编译时会报错)
  • 逻辑错误
  • 运行时错误(可能会导致闪退,一般也叫做异常)

1. 自定义错误

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)

2. 错误处理

处理Error有两种方式:

① 通过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")
}

② 不捕捉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同理。

③ 一些关键字

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 } //这里就是把let error省略了

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)

注意:throws、rethrows放在 -> 前面,代表这个函数执行完的返回结果会抛出错误。

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语句,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()
}

三. 泛型(Generics)

1. 泛型简介

泛型可以将类型参数化,提高代码复用率,减少代码量。

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写成泛型类型,可以提高代码复用率,如下:

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>() //因为不知道Stack里面泛型的真实类型,所以这里要指定类型
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协议,并不知道具体是什么宠物。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容