[程序员日记]错误和异常处理(转)

异常和错误对于很多iOS,尤其是以Objective-C为主要语言的程序员来说是经常混淆的概念。最近在学习Swift时看到这篇tip,希望与大家共勉。

文章摘自 王巍 (@onevcat) 《Swifter (第二版)100个Swift 2开发必备Tip》 tip77 错误和异常处理

作者博客:http://swifter.tips/error-handle/

转载请注明出处

在开始这一节的内容之前,我想先阐明两个在很多时候被混淆的概念,那就是异常 (exception) 和错误 (error)。

Objective-C 开发中,异常往往是由程序员的错误导致的 app 无法继续运行,比如我们向一个无法响应某个消息的 NSObject 对象发送了这个消息,会得到 NSInvalidArgumentException的异常,并告诉我们 "unrecognized selector sent to instance";比如我们使用一个超过数组元素数量的下标来试图访问 NSArray 的元素时,会得到 NSRangeException。类似由于这样所导致的程序无法运行的问题应该在开发阶段就被全部解决,而不应当出现在实际的产品中。相对来说,由 NSError 代表的错误更多地是指那些“合理的”,在用户使用 app 中可能遇到的情况:比如登陆时用户名密码验证不匹配,或者试图从某个文件中读取数据生成 NSData 对象时发生了问题 (比如文件被意外修改了) 等等。

但是 NSError的使用方式其实变相在鼓励开发者忽略错误。想一想在使用一个带有错误指针的 API 时我们做的事情吧。我们会在 API 调用中产生和传递 NSError,并藉此判断调用是否失败。作为某个可能产生错误的方法的使用者,我们用传入 NSErrorPointer 指针的方式来存储错误信息,然后在调用完毕后去读取内容,并确认是否发生了错误。比如在 Objective-C 中,我们会写类似这样的代码:

NSError *error; 
BOOL success = [data writeToFile: path options: options error: &error];
if(error) { 
       // 发生了错误
}

这非常棒,但是有一个问题:在绝大多数情况下,这个方法并不会发生什么错误,而很多工程师也为了省事和简单,会将输入的 error 设为 nil,也就是不关心错误 (因为可能他们从没见过这个 API 返回错误,也不知要如何处理)。于是调用就变成了这样:
[data writeToFile: path options: options error: nil];

但是事实上这个 API 调用是会出错的,比如设备的磁盘空间满了的时候,写入将会失败。但是当这个错误出现并让你的 app 陷入难堪境地的时候,你几乎无从下手进行调试 -- 因为系统曾经尝试过通知你出现了错误,但是你却选择视而不见。

在 Swift 2.0 中,Apple 为这么语言引入了异常机制。现在,这类带有 NSError指针作为参数的 API 都被改为了可以抛出异常的形式。比如上面的 writeToFile:options:error:,在 Swift 中变成了:

public func writeToFile(path: String, options writeOptionsMask: NSDataWritingOptions) throws

我们在使用这个 API 的时候,不再像之前那样传入一个 error 指针去等待方法填充,而是变为使用try catch 语句:

do { try d.writeToFile("Hello", options: [])} catch let error as NSError { print ("Error: \(error.domain)")}

如果你不使用 try 的话,是无法调用 writeToFile: 方法的,它会产生一个编译错误,这让我们无法有意无意地忽视掉这些错误。在上面的示例中 catch 将抛出的异常 (这里就是个 NSError) 用 let 进行了类型转换,这其实主要是针对 Cocoa 现有的 API 的,是对历史的一种妥协。对于我们新写的可抛出异常的 API,我们应当抛出一个实现了 ErrorType 的类型,enum 就非常合适,举个例子:

enum LoginError: ErrorType { 
  case UserNotFound, UserPasswordNotMatch
  }
  func login(user: String, password: String) throws { 
     //users 是 [String: String],存储[用户名:密码] 
     if !users.keys.contains(user) { 
          throw LoginError.UserNotFound
     } 
     if users[user] != password {
          throw LoginError.UserPasswordNotMatch 
     } 
     print("Login successfully.")
}

这样的 ErrorType 可以非常明确地指出问题所在。在调用时,catch语句实质上是在进行模式匹配:

do {
 try login("onevcat", password: "123")
} catch LoginError.UserNotFound { 
print("UserNotFound")
} catch LoginError.UserPasswordNotMatch {     print("UserPasswordNotMatch")
}// Do something with login user

如果你之前写过 Java 或者 C# 的话,会发现 Swift 中的try catch 块和它们中的有些不同。在那些语言里,我们会把可能抛出异常的代码都放在一个 try 里,而 Swift 中则是将它们放在 do 中,并只在可能发生异常的语句前添加 try。相比于 Java 或者 C# 的方式,Swift 里我们可以更清楚地知道是哪一个调用可能抛出异常,而不必逐句查阅文档。

当然,Swift 现在的异常机制也并不是十全十美的。最大的问题是类型安全,不借助于文档的话,我们现在是无法从代码中直接得知所抛出的异常的类型的。比如上面的 login 方法,光看方法定义我们并不知道 LoginError 会被抛出。一个理想中的异常 API 可能应该是这样的:

func login(user: String, password: String) throws LoginError

很大程度上,这是由于要与以前的 NSError 兼容所导致的妥协,对于之前的使用 NSError 来表达错误的 API,我们所得到的错误对象本身就是用像 domain 或者 error number 这样的属性来进行区分和定义的,这与 Swift 2.0 中的异常机制所抛出的直接使用类型来描述错误的思想暂时是无法兼容的。不过有理由相信随着 Swift 的迭代更新,这个问题会在不久的将来得到解决。
另一个限制是对于非同步的 API 来说,抛出异常是不可用的 -- 异常只是一个同步方法专用的处理机制。Cocoa 框架里对于异步 API 出错时,保留了原来的NSError 机制,比如很常用的 NSURLSession 中的 dataTask API:

func dataTaskWithURL(_ url: NSURL, completionHandler completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionDataTask

对于异步 API,虽然不能使用异常机制,但是因为这类 API 一般涉及到网络或者耗时操作,它所产生错误的可能性要高得多,所以开发者们其实无法忽视这样的错误。但是像上面这样的 API 其实我们在日常开发中往往并不会去直接使用,而会选择进行一些封装,以求更方便地调用和维护。一种现在比较常用的方式就是借助于 enum。作为 Swift 的一个重要特性,枚举 (enum) 类型现在是可以与其他的实例进行绑定的,我们还可以让方法返回枚举类型,然后在枚举中定义成功和错误的状态,并分别将合适的对象与枚举值进行关联:

enum Result { 
   case Success(String) case Error(NSError)
}
func doSomethingParam(param:AnyObject) -> Result { 
    //...做某些操作,成功结果放在 success 中 
    if success { 
         return Result.Success("成功完成") 
    } else { 
         let error = NSError(domain: "errorDomain", code: 1, userInfo: nil) return Result.Error(error) 
   }
}

在使用时,利用 switch 中的 let 来从枚举值中将结果取出即可:

let result = doSomethingParam(path)
switch result { 
    case let .Success(ok): 
         let serverResponse = okcase 
         let .Error(error):
         let serverResponse = error.description
}

在 Swift 2.0 中,我们甚至可以在 enum 中指定泛型,这样就使结果统一化了。

enum Result<T> { case Success(T) case Failure(NSError)}

我们只需要在返回结果时指明 T 的类型,就可以使用同样的 Result 枚举来代表不同的返回结果了。这么做可以减少代码复杂度和可能的状态,同时不是优雅地解决了类型安全的问题,可谓一举两得。
因此,在 Swift 2 时代中的错误处理,现在一般的最佳实践是对于同步 API 使用异常机制,对于异步 API 使用泛型枚举。

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

推荐阅读更多精彩内容

  • error code(错误代码)=0是操作成功完成。error code(错误代码)=1是功能错误。error c...
    Heikki_阅读 3,379评论 1 9
  • 章节导航:Swift开发指南:使用Swift与Cocoa和Objective-C(Swift 4) - 1.入门S...
    Minecode阅读 3,189评论 0 23
  • error code(错误代码)=2000是无效的像素格式。error code(错误代码)=2001是指定的驱动...
    Heikki_阅读 1,796评论 0 4
  • 工作既是谋生手段,也是一个人对社会的一份责任。那责任到底是什么?所谓责任,就是人们在各自的岗位上尽职尽责,工作到位...
    zhaohlyd阅读 1,613评论 0 1
  • 还要我说什么 说什么徒劳的言语 说什么海枯石烂 说什么与尔白头 我早已抛弃了神灵 寺庙的佛对我 怒目而视 我恍若未...
    皇氏三墳阅读 235评论 0 1