iOS-Swift-錯誤處理拼缝、泛型

一. 錯誤處理

開發(fā)中常見的錯誤:

  • 語法錯誤(編譯時會報錯)
  • 邏輯錯誤
  • 運行時錯誤(可能會導致閃退争占,一般也叫做異常)

1. 自定義錯誤

Swift中可以通過遵守Error協(xié)議自定義運行時的錯誤信息,無論是枚舉郑兴、結(jié)構(gòu)體坐儿、類只要遵守Error協(xié)議都可以當做錯誤信息律胀,Error協(xié)議源碼:

public protocol Error {
}
enum SomeError : Error {
    case illegalArg(String)
    case outOfBounds(Int, Int)
    case outOfMemory
}

函數(shù)內(nèi)部通過throw拋出自定義Error,可能會拋出Error的函數(shù)必須加上throws聲明

func divide(_ num1: Int, _ num2: Int) throws -> Int {
    if num2 == 0 {
        throw SomeError.illegalArg("0不能作為除數(shù)")
    }
    return num1 / num2
}

需要使用try調(diào)用可能會拋出Error的函數(shù)

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("參數(shù)異常:", msg)
    } catch let SomeError.outOfBounds(size, index) {
        print("下標越界:", "size=\(size)", "index=\(index)")
    } catch SomeError.outOfMemory {
        print("內(nèi)存溢出")
    } catch {
        print("其他錯誤") }
    print("4")
}

test()
// 1
// 2
// 參數(shù)異常: 0不能作為除數(shù)
// 4

沒有Error情況下挑童,上面打印是“1累铅,2,3站叼,4”娃兽,拋出Error后,try下一句直到作用域結(jié)束的代碼都將停止運行尽楔,通過上面的打印也可以驗證投储。

下面代碼也可以捕獲所有error,然后用switch區(qū)分是哪個error阔馋,道理都是一樣的玛荞,如下:

do {
    try divide(20, 0)
} catch let error { //可以把let error省略,因為catch后默認就有?個error給你使?
    switch error {
    case let SomeError.illegalArg(msg):
        print("參數(shù)錯誤:", msg)
    default: 
        print("其他錯誤")
    }
}

可以把let error省略呕寝,因為catch后默認就有?個error給你使?勋眯,如下:

do {
    try divide(20, 0)
} catch is SomeError {
    print("SomeError")
}

② 不捕捉Error,在當前函數(shù)增加throws聲明,Error將自動拋給上層函數(shù)

如果最頂層函數(shù)(main函數(shù))依然沒有捕捉Error客蹋,那么程序?qū)⒔K止

func test() throws {
    print("1")
    print(try divide(20, 0))
    print("2")
}

try test()
// 1
// Fatal error: Error raised at top level

補充:如果test1調(diào)用test2塞蹭,test2調(diào)用divide,divide會拋出異常讶坯,按理說要求test2要把所有錯誤處理完番电,如果test2不處理錯誤或者只處理一部分錯誤,都要使用throws把錯誤往上拋出去辆琅,如果test2處理完所有錯誤漱办,就不需要throws了,test1同理婉烟。

③ 一些關鍵字

try?娩井、try!

可以使用try?、try!調(diào)用可能會拋出Error的函數(shù)隅很,這樣就不用去處理Error撞牢。
try?、try!會自動處理叔营,對于try?,如果拋出錯誤就返回nil所宰,最后結(jié)果是可選類型绒尊,對于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表明:函數(shù)本身不會拋出錯誤,但調(diào)用閉包參數(shù)拋出錯誤躯泰,那么它會將錯誤向上拋

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放在 -> 前面,代表這個函數(shù)執(zhí)行完的返回結(jié)果會拋出錯誤麦向。

defer

defer語句:用來定義以任何方式(包括拋錯誤瘟裸、return等)離開代碼塊前必須要執(zhí)行的代碼。就算defer語句寫在前面诵竭,也將延遲至當前作用域結(jié)束之前執(zhí)行话告。

舉例:比如文件操作的時候,最后必須要關閉文件

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將會在這里調(diào)用
}
    
try processFile("test.txt")
// open
// close
// Fatal error: Error raised at top level

如果上面的 try divide(20, 0) 操作拋出了錯誤卵慰,那么后面的關閉文件的代碼就不會執(zhí)行了沙郭,所以要使用defer語句,把關閉文件的代碼寫在defer語句里面裳朋,保證無論如何最后都會執(zhí)行病线。

注意:如果有多個defer語句,defer語句的執(zhí)行順序與定義順序相反,如下:

func fn1() { print("fn1") }
func fn2() { print("fn2") }
func test() {
    defer { fn1() }
    defer { fn2() }
}
test()
// fn2
// fn1

assert(斷言)

很多編程語言都有斷言機制:不符合指定條件就拋出運行時錯誤送挑。
因為自定義拋出的錯誤可以捕捉绑莺,但是斷言不能捕捉,所以斷言常用于調(diào)試(Debug)階段的條件判斷让虐。
默認情況下紊撕,Swift的斷言只會在Debug模式下生效,Release模式下會忽略赡突。

func divide(_ v1: Int, _ v2: Int) -> Int {
    assert(v2 != 0, "除數(shù)不能為0")
    return v1 / v2
}
print(divide(20, 0))
// Assertion failed: 除數(shù)不能為0: file MyPlayground.playground, line 2

增加Swift Flags对扶,修改斷言的默認行為,如下圖:
-assert-config Release:強制關閉斷言
-assert-config Debug:強制開啟斷言

斷言

fatalError

如果遇到嚴重問題希望結(jié)束程序運行惭缰,可以直接使用fatalError函數(shù)拋出錯誤(這也是無法通過do-catch捕捉的錯誤)浪南。
使用了fatalError函數(shù),就不需要再寫return漱受。

func test(_ num: Int) -> Int {
    if num >= 0 {
        return 1
    }
    fatalError("num不能小于0")
}

在某些不得不實現(xiàn)络凿、但不希望別人調(diào)用的方法,可以考慮內(nèi)部使用fatalError函數(shù)

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 {} 實現(xiàn)局部作用域

do {
    let dog1 = Dog()
    dog1.age = 10
    dog1.run()
}
    
do {
    let dog2 = Dog()
    dog2.age = 10
    dog2.run()
}

三. 泛型(Generics)

1. 泛型簡介

泛型可以將類型參數(shù)化絮记,提高代碼復用率,減少代碼量虐先。

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

泛型函數(shù)賦值給變量撰洗,要在:后面明確類型,如下:

func test<T1, T2>(_ t1: T1, _ t2: T2) {} 
var fn: (Int, Double) -> () = test

因為只是把泛型函數(shù)賦值給變量腐芍,等到調(diào)用函數(shù)的時候還是不知道函數(shù)的類型差导,所以要求泛型函數(shù)賦值給變量的時候要指定類型。

2. 泛型使用舉例

//進棧出棧操作:
class Stack<E> {
    var elements = [E]() //泛型數(shù)組初始化器
    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> {}
//如果定義成結(jié)構(gòu)體设褐,需要加上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. 泛型的本質(zhì)

泛型內(nèi)部是一個函數(shù)還是根據(jù)傳入不同的類型生成不同的函數(shù)呢埠对?

如下代碼络断,如果下面兩個函數(shù)地址一樣說明是一個函數(shù),如果兩個函數(shù)地址不一樣說明生成了兩個不同的函數(shù)

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) //斷點

打斷點项玛,查看匯編貌笨,發(fā)現(xiàn)兩個函數(shù)地址一樣,說明還是一個函數(shù)襟沮,沒有根據(jù)類型不同生成不同的函數(shù)锥惋。

那么是怎么做到共用一個函數(shù)解決問題的呢昌腰?
通過查看匯編可以發(fā)現(xiàn),是把泛型類型的元信息傳進去了膀跌,這樣swapValues函數(shù)內(nèi)部就會做相應的處理遭商。

4. 協(xié)議中的關聯(lián)類型(Associated Type)

協(xié)議中的關聯(lián)類型的作用:給協(xié)議中用到的類型定義一個占位名稱。
協(xié)議中可以擁有多個關聯(lián)類型捅伤。

protocol Stackable {
    associatedtype Element //協(xié)議中使用泛型劫流,用關聯(lián)類型
    mutating func push(_ element: Element)
    mutating func pop() -> Element
    func top() -> Element
    func size() -> Int
}

//如果是String類型
class StringStack : Stackable {
    // 給關聯(lián)類型設定真實類型,下面可以推導出來就可以省略
    // 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() //因為已經(jīng)知道了泛型的真實類型丛忆,所以這里不用指定類型
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協(xié)議凰浮,如下:

protocol Runnable { }
class Person { }
func swapValues<T : Person & Runnable>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}

更復雜的泛型類型約束我抠,如下:

//協(xié)議里面的泛型,要求遵守Equatable協(xié)議
protocol Stackable {
    associatedtype Element: Equatable
}
//寫成泛型袜茧,并遵守Equatable協(xié)議
class Stack<E : Equatable> : Stackable { typealias Element = E }

//要求菜拓,S1、S2都要遵守Stackable協(xié)議笛厦,并且S1尘惧、S2的關聯(lián)類型相等,并且S1的關聯(lián)類型遵守Hashable協(xié)議
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的關聯(lián)類型是Int递递,S2的關聯(lián)類型是String,關聯(lián)類型不相等
equal(stack1, stack2)

6. 協(xié)議類型的注意點

返回Runnable協(xié)議啥么,就是返回遵守Runnable協(xié)議的東西登舞。

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)

如果上面協(xié)議中有關聯(lián)類型,如下:

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協(xié)議里面泛型的關聯(lián)類型是什么氯迂,只有在運行過程中才會知道泛型的關聯(lián)類型(r1是Double践叠,r2是Int)。

1.png

解決方案①:使用泛型

func get<T : Runnable>(_ type: Int) -> T { //返回一個遵守Runnable協(xié)議的東西
    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限制只能返回一種類型,既然只能返回一種類型轿曙,那么編譯器肯定知道關聯(lián)類型是什么了弄捕,所以上面那樣寫不會報錯僻孝。如果返回兩種類型會報錯,如下:

3.png

用處:我只想讓外界知道我返回的是?個遵守這個協(xié)議的類型守谓,但是真實是什么類型我不告訴你穿铆,你只能拿到我返回給你的類型調(diào)?協(xié)議??的東?,這也是叫不透明類型的原因斋荞。

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協(xié)議里面有個關聯(lián)類型平酿,Person想養(yǎng)一個寵物凤优,這個寵物要求要遵守Runnable協(xié)議,但是具體養(yǎng)什么寵物不想讓你知道染服,這時候就可以用some别洪,然后返回Dog(),這時候外界只知道你的寵物遵守Runnable協(xié)議柳刮,并不知道具體是什么寵物挖垛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市秉颗,隨后出現(xiàn)的幾起案子痢毒,更是在濱河造成了極大的恐慌,老刑警劉巖蚕甥,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哪替,死亡現(xiàn)場離奇詭異,居然都是意外死亡菇怀,警方通過查閱死者的電腦和手機凭舶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爱沟,“玉大人帅霜,你說我怎么就攤上這事『羯欤” “怎么了身冀?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長括享。 經(jīng)常有香客問我搂根,道長,這世上最難降的妖魔是什么铃辖? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任剩愧,我火速辦了婚禮,結(jié)果婚禮上澳叉,老公的妹妹穿的比我還像新娘隙咸。我一直安慰自己沐悦,他們只是感情好,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布五督。 她就那樣靜靜地躺著藏否,像睡著了一般。 火紅的嫁衣襯著肌膚如雪充包。 梳的紋絲不亂的頭發(fā)上副签,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音基矮,去河邊找鬼淆储。 笑死,一個胖子當著我的面吹牛家浇,可吹牛的內(nèi)容都是我干的本砰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼钢悲,長吁一口氣:“原來是場噩夢啊……” “哼点额!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起莺琳,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤还棱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后惭等,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珍手,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年辞做,在試婚紗的時候發(fā)現(xiàn)自己被綠了琳要。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡秤茅,死狀恐怖焙蹭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嫂伞,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布拯钻,位于F島的核電站帖努,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏粪般。R本人自食惡果不足惜拼余,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亩歹。 院中可真熱鬧匙监,春花似錦凡橱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至达罗,卻和暖如春坝撑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粮揉。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工巡李, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扶认。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓侨拦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辐宾。 傳聞我的和親對象是個殘疾皇子狱从,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容