美文网首页
iOS-Swift-错误处理、泛型

iOS-Swift-错误处理、泛型

作者: Imkata | 来源:发表于2020-01-14 14:56 被阅读0次

一. 错误

  • 错误类型

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

  • 自定义错误

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协议,并不知道具体是什么宠物。

相关文章

  • iOS-Swift-错误处理、泛型

    一. 错误 错误类型 开发中常见的错误:语法错误(编译时会报错)逻辑错误运行时错误(可能会导致闪退,一般也叫做异常...

  • 异步任务的极简处理

    使用示例 实现机制:泛型方法+高阶函数 它有默认的错误处理,也可以自定义错误处理,默认的话,只是打印一下错误,可以...

  • (WWDC) Swift 和 Objective-C 的互操作性

    内容概览 Swift 与 Objective-C 交互 错误处理 为空性标注 轻量级泛型 Kindof 类型 总结...

  • Swift错误处理和泛型(ErrorHandlingAndGen

    错误处理 泛型 ErrorHandling defer使用关键字defer写一个代码块,它会在这个函数内所有代码执...

  • Swift错误处理和泛型

    1. 错误处理, 可以用任何遵循Error协议的类型来表示错误. 可以用throw来抛出一个错误, 并用throw...

  • swift从入门到放弃-初步接触(1)

    初步接触可变参数枚举结构体错误处理泛型 由于项目需要和苹果发展的趋势,swift的学习势在必得,swift以其优秀...

  • 泛型 & 注解 & Log4J日志组件

    掌握的知识 : 基本用法、泛型擦除、泛型类/泛型方法/泛型接口、泛型关键字、反射泛型(案例) 泛型 概述 : 泛型...

  • 【泛型】通配符与嵌套

    上一篇 【泛型】泛型的作用与定义 1 泛型分类 泛型可以分成泛型类、泛型方法和泛型接口 1.1 泛型类 一个泛型类...

  • 泛型的使用

    泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法 泛型类 泛型接口 泛型通配符 泛型方法 静态方法与...

  • Java 泛型

    泛型类 例如 泛型接口 例如 泛型通配符 泛型方法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型上下边...

网友评论

      本文标题:iOS-Swift-错误处理、泛型

      本文链接:https://www.haomeiwen.com/subject/attnactx.html