一. 錯誤處理
開發(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)。
解決方案①:使用泛型
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)類型是什么了弄捕,所以上面那樣寫不會報錯僻孝。如果返回兩種類型會報錯,如下:
用處:我只想讓外界知道我返回的是?個遵守這個協(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é)議柳刮,并不知道具體是什么寵物挖垛。