Siwft開發之--自定義異常

    在軟件開發的過程中,我們會儘可能保證每一個業務模塊都能夠正確的執行,但是天有不測風雲,即使在代碼中增加了很多if...else或者switch...case 的判斷,在某些極端的情況下,程序已經運行在編程者的思維邊緣,導致一些詭異的行爲,例如在進行http請求的時候,我們很確定某一個數據有值且是期望的值,但是突然某一天程序就因爲這個理所當然導致bug的產生,於是焦頭爛額一整天也很難找到原因,上線的app如果沒有加入全埋點的log日誌,更難定位bug產生的原因,即使找到了原因,由於時隔久遠,那一堆堆if...else的判斷,也會讓一個聰明的程序員變成丈二的和尚。所以在處理相關錯誤信息或者與預期不同的情況時,加入自定義的異常判斷和處理機制無疑是一種優雅的創作方式,swift的異常處理可以說是優雅、優雅、還有優雅。

1.什麼是Error

Error是一個協議,也就是一個接口,作爲異常類、枚舉、結構體的公共接口,想要產生(也就是throw)一個異常,需要先實現這個接口,具體內容由實際業務決定,例如我們從服務器獲取數據時,我們已經定義一個整數數值的取值範圍爲0-100之間,任何超過或者低於這個範圍的值,都代表錯誤:

func checkX(x: Int) -> Bool {
    return x >= 0 && x <= 100
}
// continue

但是由於這個數值本身就是錯誤的,上面的checkX方法僅僅是返回了Bool類型,並沒有任何的信息記錄,實際中可能會造成很嚴重的問題(該條可能是由於數據遭人篡改,可能是某種情況下解析數據時不太容易察覺的bug,也可能由插入數據庫中的信息錯誤),我們需要將這個問題升級爲一個真正的問題(也就是一個異常)。
首先定義一個Error

enum XErrorRange: Error {
    case tooLowwerXError
    case toLargeXError
    case unKnowCaseError
}

以上代碼通過枚舉定義了一個錯誤枚舉類型XErrorRange,tooLowwerXError、toLargeXError以及unKnowCaseError,上面說過,Error是一個接口,該接口沒有實現任何東西,只是告訴swift的錯誤處理系統,XErrorRange是一個Error,可以被拋出(throw)或者當做異常處理。Error我們實現了,那麼如何使用呢?

2.拋出一個異常

swift通過throw 關鍵字拋出一個異常,例如:throw XErrorRange.toLargeXError ,系統就會拋出一個數值過大的異常,重新改造checkX函數使其具備拋出異常的能力,當參數x的值在0到100之間時,調用doSomething函數,否則拋出異常:

func checkX(x: Int , doSomethigh: ()->Void) throws {
//    return x >= 0 && x <= 100
    switch x {
    case 0...100 :
        doSomethigh()
    case let tmp where tmp > 100 :
        throw XErrorRange.toLargeXError  // ①
    case let tmp where tmp < 0 :
        throw XErrorRange.tooLowwerXError  // ②
    default:
        throw XErrorRange.unKnowCaseError  // ③
    }
}

程序通過throw 關鍵字拋出一個異常,throw X,其中x是一個實現了Error協議的對象。而使用throws (放在->之前)關鍵字可以標記一個函數會拋出異常,調用時需要捕獲
①處表示當x大於100時,向調用出拋出toLargeXError。
②處表示當x小於0時,拋出toLargeXError。
③處表示未知錯誤,例如非數字等情況的發生。
那麼此時這個方法就表示在調用時可能會產生異常,調用checkX的函數一定要對其進行異常捕獲,那麼如何捕獲異常呢?

3.異常捕獲

通過try來標記能夠拋出異常的執行代碼,並使用 do-catch 語句捕獲異常。如果我們調用checkX方法,就需要對其進行try標記,並在do-catch中捕獲異常:

do {
    try checkX(x: -1, doSomethigh: {
        print("OK")
    })   // ①
} catch XErrorRange.tooLowwerXError {
    print("too lowwer")
} catch XErrorRange.toLargeXError {
    print("too Large")
} catch XErrorRange.unKnowCaseError {
    print("Unknow size")
} catch let error  {                  ②
    print("Oh no!! \(error)")
}

①處 通過try關鍵字標記checkX方法是可以拋出異常的,在catch語句中標記出所有可能的異常。
②處 catch語句捕獲未知的異常,當所有其他的catch都已經遍歷且都不與此時的error匹配時,通過一個大範圍的異常捕獲可以兜住未知的錯誤。

4. 避免捕獲

我們可以只標記異常的源頭,並不進行實際的捕獲(不用do-catch捕獲),例如:

enum NilError: Error {
    case nilError
}

func checkNil<T>(t: T?)  throws -> T{     //①
    if t == nil {
        throw NilError.nilError
    }
    return t!
}

var d: Int?

var c = try? checkNil(t: d)         // ②
var e = try! checkNil(t: 5)         // ③

①處的checkNil函數檢查一個對象是否爲nil,如果對象爲空,則拋出NilError異常。
②處對checkNil進行try標記,但是由於是通過try?標記,所以如果checkNil有異常拋出,那麼c就被賦值爲nil,並不產生運行時錯誤。
③處代碼類似於斷言,斷言調用的checkNil函數一定不會產生異常,否則產生runtime error。
通過try?、try!標記,可以省去沉長的異常捕獲語句,但是也降低了代碼的健壯性。

5.defer語句

有時候我們希望,無論函數是否拋出異常,我們都要做一些事情,例如文件流的關閉、釋放內存空間等,可以使用defer語句,defer語句的作用是在代碼作用域返回(throw、return、break)前調用defer中的代碼塊:

func throwDemo() throws {
    var pointer = UnsafeMutablePointer<Int>.allocate(capacity: 10)
    // throw some error
    defer {
        pointer.deallocate(capacity: 10)
    }
}

函數在結束之前,會回收pointer指向的內存,無論是否拋出異常。

6.rethrows關鍵字

一個函數的異常僅僅是因爲參數造成的(閉包),那麼我們可以將方法聲明爲rethrows,意味着此函數產生的異常僅僅是因爲函數參數能夠拋出異常。

func rethrowError(_ test : @escaping () throws -> Void) rethrows {
    try test()
}

由於test是一個能夠拋出異常的閉包,而rethrowError方法僅在test拋出異常時纔會產生異常,所以採用rethrows關鍵字更貼切。(注意如果不是函數參數造成的異常,必須使用throws關鍵字)



需要注意在swift中調用OC方法是,關於do...catch有一點點瑕疵,例如:

//OCFileTest.h
@interface OCFileTest : NSObject
- (NSString *)test:(NSError **)error;
- (NSString *)testNoError;
@end

//OCFileTest.m
#import "OCFileTest.h"

@implementation OCFileTest
- (nullable NSString *)test:(NSError **)error {
     return nil;
    *error = [NSError errorWithDomain:@"Error" code:100 userInfo:nil];
   
}

- (NSString *)testNoError {
    return nil;
}
@end

swift在翻譯的時候會自動將(NSError **)error參數看成是異常拋出,需要使用try 捕獲:

let ocf = OCFileTest.init()
var e = ocf.testNoError() // 沒有try時,返回nil沒有異常
do {
      try ocf.test() // 當沒有error時返回nil,會拋出nilError
 } catch {
      print("error is \(error)")
      print("error is \(error.localizedDescription)")
}

在test方法中我們僅僅返回了nil,但是這樣,由於swift的問題,將nil定義成了異常最後會輸出:

error is nilError
error is The operation couldn’t be completed. (Foundation._GenericObjCError error 0.)

即使在返回類型上指定nullable也會捕獲到nilError異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章