是个人就会犯错。 用 Cocoa 就会用到
NSError。
Unix 系统中所有的程序都是其他进程的子进程,从原始进程一路分支(fork)出来,即那个不为所动的发起者:pid 1 (OS X 系统中是 launchd)。当可执行文件结束时,它用一个 0 到 255 状态码与父进程通信,告诉它程序为什么或怎么退出的。0 意味着“一切正常退出;没有可通告的”,非零值表示发生了父进程需要注意的事。退出状态码可以用来表示进程是否崩溃,或者提前终止。按照惯例,越大的值,引发的错误越严重。
在面向对象编程范式中,大部分进程被抽象出来,只剩下对象及它们之间传递的消息。但是成功还是失败(以及不同种类的失败)在面向对象编程中仍然十分有用。但是考虑到方法通常不会返回非 BOOL 值,这就要创造一个新的东西还解决这个困境了。
那些比 Objective-C 更小题大做更好战的语言通过滥用异常来调解这个问题,哪怕一点点的不合规也抛异常。不过作为果粉幸运的是,当有坏事发生时 Objective-C 使用一种更文雅的方式,那便是 NSError。
NSError 是基础框架中的无名英雄。勇敢地传入传出危险的方法调用,通过消息发送者我们就可以从所关联的上下文中找出我们的失败。当涉及到这样的问题时肯定没什么好事,但是传入 nil 到 NSError ** 参数中并没有什么卵用。
NSError 是 CFError 的无缝转换对象,但是你没理由会去深挖它在 Core Foundation 中对应的部分。
每个 NSError 对象编码了三部分关键的信息:状态码 code,对应的特定错误域 domain,还有额外的通过 userInfo 字典提供的上下文。
code 和 domain与退出状态码不同的是,NSError -code 表示问题的本质。这些状态码都在一个特定的错误域 domain 中定义,以防重叠和混淆。这些状态码一般用 enum 来定义。
例如,在 NSCocoaErrorDomain 中,由 NSFileManager 访问一个不存在的文件产生的错误状态码是 4,正如 NSFileNoSuchFileError 所定义的。然而,4 在 NSPOSIXErrorDomain 中指代 POSIX EINTR,或者“中断函数”错误。
如今,任何有系统编程背影的人也许会只用一个 switch 语句加上少量的 printf 把数字常量翻译为人类可以阅读的信息。NSError 永远领先于你。
userInfo是什么给了 NSError 它独特的魅力?正是这个每个人都喜欢的大杂烩属性:userInfo。作为整个 Cocoa 的惯例,userInfo 是一个可以包含任意键值对的字典,无论是为了继承或降低耦合的目的, 它都不适合拿来填满各种杂七杂八的属性。在 NSError 这个例子中,有一些特定的键值对应着只读属性。
这三个通常很有用:
localizedDescription (NSLocalizedDescriptionKey): 一段本地化的错误描述。localizedRecoverySuggestion (NSLocalizedRecoverySuggestionErrorKey): 一段该错误的恢复建议。localizedFailureReason (NSLocalizedFailureReasonErrorKey): 一段本地化的错误解释。而另外三个只用在 OS X:
localizedRecoveryOptions (NSLocalizedRecoveryOptionsErrorKey): 一个包含了本地化的按钮标题的数组,用于展示在警告框中。recoveryAttempter (NSRecoveryAttempterErrorKey)helpAnchor (NSHelpAnchorErrorKey): 用于警告框中的帮助按钮。以下是如何用 userInfo 字典来构造一个 NSError:
NSDictionary *userInfo = @{
  NSLocalizedDescriptionKey: NSLocalizedString(@"Operation was unsuccessful.", nil),
  NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The operation timed out.", nil),
  NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Have you tried turning it off and on again?", nil)
                          };
NSError *error = [NSError errorWithDomain:NSHipsterErrorDomain
                                     code:-57
                                 userInfo:userInfo];
相比于无可奈何地抛出异常,将这些信息包装在一个类似于 NSError 这样的对象中的优势在于,这些错误对象可以很容易的在不同对象及上下文中传递。
举个例子,一个 controller 调用一个参数为 NSError ** 的方法(下一节将会讨论)可以将那个错误传到警告框中:
[[[UIAlertView alloc] initWithTitle:error.localizedDescription
                            message:error.localizedRecoverySuggestion
                           delegate:nil
                  cancelButtonTitle:NSLocalizedString(@"OK", nil)
                  otherButtonTitles:nil, nil] show];
作为一个合乎逻辑的推断(?):一个在 C 函数中使用的交流错误的聪明办法是将四个字母的 ASCII 码序列编码为 32 比特的返回类型 . 它不是
本地化的描述,但至少比每次去找一个交叉引用的错误码表要好!
为了完整性:以下是标准 NSError 的 userInfo 的键列表:
NSLocalizedDescriptionKeyNSLocalizedFailureReasonErrorKeyNSLocalizedRecoverySuggestionErrorKeyNSLocalizedRecoveryOptionsErrorKeyNSFilePathErrorKeyNSStringEncodingErrorKeyNSUnderlyingErrorKeyNSRecoveryAttempterErrorKeyNSHelpAnchorErrorKeyNSError你会在两种情况下遇到 NSError:作为消费者,或者作为生产者。
为作消费者,你主要关心的是那些最后一个参数类型是 NSError ** 的方法。同样,这是一种规避 Objective-C 单一返回值的手段;通过传递一个指向未初始化的 NSError * 变量的指针,那个变量将会被填入方法调用过程中遇到的错误:
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager] moveItemAtPath:@"/path/to/target"
                                                       toPath:@"/path/to/destination"
                                                        error:&error];
if (!success) {
    NSLog(@"%@", error);
}
根据 Cocoa 的惯例,鼓励那些返回
BOOL来表示成功或失败的方法使用NSError **作为它的最后一个参数,如果有需要作区分的多种可能的失败条件的话。一个好的办法是,你是否能想像那个NSError将不断向上冒泡,最终呈现给用户。
另一种传递 NSError 的方式是把它做为 completionHandler 回调的一个参数。这样即可以规避单一返回值的弊端,也可以避免值被同步返回。这种方式在新的 Foundation API 中特别流行,比如 NSURLSession:
NSURL *URL = [NSURL URLWithString:@"http://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
[[session dataTaskWithRequest:request
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error) {
        NSLog(@"%@", error);
    } else {
        // ...
    }
}] resume];
一般非常建议开发者按照其它 Foundation 类库的惯例来处理错误。在一个自定义的方法中调用了另一个带有 NSError ** 形参的方法的情形下,通常同样地把 NSError ** 参数传入自定义方法是个好的做法。鼓励 app 或第三方库适当地定义自己的错误域及错误代码常量。
传递一个错误到 NSError ** 参数,做以下事情:
- (BOOL)validateObject:(id)object
                 error:(NSError * __autoreleasing *)error
{
    BOOL success = ...;
    if (!success) {
      if (error) {
        *error = [NSError errorWithDomain:NSHipsterErrorDomain
                                     code:-42
                                 userInfo:nil];
      }
    }
    return success;
}
NSURLErrorDomain 和 CFNetworkErrorsiOS 应用中最大的失败来源是网络。在无线信号,信号传输,数据漫游,代理,安全性,认证以及各种协议协商中,每个过程都可能出错。
好的一面是,Foundation 中的 URL Loading 系统相当成熟,它接管了大部分事情。唯一不足的是不同的出错信息的文档分散在不同的编程指南和头文件中。如果你收到一个 -1004 的失败信息,你很难找出它是什么意思。
正因如此,这里有一份详尽的、格式化的表格供你查阅:
| Code | Description | 
|---|---|
| -1 NSURLErrorUnknown | |
| 1 kCFHostErrorHostNotFound | Indicates that the DNS lookup failed. | 
| 2 kCFHostErrorUnknown | An unknown error occurred (a name server failure, for example). For additional information, query the kCFGetAddrInfoFailureKey to get the value returned from getaddrinfo; lookup in netdb.h | 
| 100 kCFSOCKSErrorUnknownClientVersion | The SOCKS server rejected access because it does not support connections with the requested SOCKS version.Query kCFSOCKSStatusCodeKey to recover the status code returned by the server. | 
| 101 kCFSOCKSErrorUnsupportedServerVersion | The version of SOCKS requested by the server is not supported. Query kCFSOCKSStatusCodeKey to recover the status code returned by the server. | 
| Code | Description | 
|---|---|
| 110 kCFSOCKS4ErrorRequestFailed | Request rejected or failed by the server. | 
| 111 kCFSOCKS4ErrorIdentdFailed | Request rejected because SOCKS server cannot connect to identd on the client. | 
| 112 kCFSOCKS4ErrorIdConflict | Request rejected because the client program and identd report different user-ids. | 
| 113 kCFSOCKS4ErrorUnknownStatusCode | The status code returned by the server is unknown. | 
| Code | Description | 
|---|---|
| 120 kCFSOCKS5ErrorBadState | The stream is not in a state that allows the requested operation. | 
| 121 kCFSOCKS5ErrorBadResponseAddr | The address type returned is not supported. | 
| 122 kCFSOCKS5ErrorBadCredentials | The SOCKS server refused the client connection because of bad login credentials. | 
| 123 kCFSOCKS5ErrorUnsupportedNegotiationMethod | The requested method is not supported. Query kCFSOCKSNegotiationMethodKey to find the method requested. | 
| 124 kCFSOCKS5ErrorNoAcceptableMethod | The client and server could not find a mutually agreeable authentication method. | 
| Code | Description | 
|---|---|
| 200 kCFFTPErrorUnexpectedStatusCode | The server returned an unexpected status code. Query the kCFFTPStatusCodeKey to get the status code returned by the server | 
| Code | Description | 
|---|---|
| 300 kCFErrorHTTPAuthenticationTypeUnsupported | The client and server could not agree on a supported authentication type. | 
| 301 kCFErrorHTTPBadCredentials | The credentials provided for an authenticated connection were rejected by the server. | 
| 302 kCFErrorHTTPConnectionLost | The connection to the server was dropped. This usually indicates a highly overloaded server. | 
| 303 kCFErrorHTTPParseFailure | The HTTP server response could not be parsed. | 
| 304 kCFErrorHTTPRedirectionLoopDetected | Too many HTTP redirects occurred before reaching a page that did not redirect the client to another page. This usually indicates a redirect loop. | 
| 305 kCFErrorHTTPBadURL | The requested URL could not be retrieved. | 
| 306 kCFErrorHTTPProxyConnectionFailure | A connection could not be established to the HTTP proxy. | 
| 307 kCFErrorHTTPBadProxyCredentials | The authentication credentials provided for logging into the proxy were rejected. | 
| 308 kCFErrorPACFileError | An error occurred with the proxy autoconfiguration file. | 
| 309 kCFErrorPACFileAuth | The authentication credentials provided by the proxy autoconfiguration file were rejected. | 
| 310 kCFErrorHTTPSProxyConnectionFailure | A connection could not be established to the HTTPS proxy. | 
| 311 kCFStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod | The HTTPS proxy returned an unexpected status code, such as a 3xx redirect. | 
| Code | Description | 
|---|---|
| -998 kCFURLErrorUnknown | An unknown error occurred. | 
| -999 kCFURLErrorCancelledNSURLErrorCancelled | The connection was cancelled. | 
| -1000 kCFURLErrorBadURLNSURLErrorBadURL | The connection failed due to a malformed URL. | 
| -1001 kCFURLErrorTimedOutNSURLErrorTimedOut | The connection timed out. | 
| -1002 kCFURLErrorUnsupportedURLNSURLErrorUnsupportedURL | The connection failed due to an unsupported URL scheme. | 
| -1003 kCFURLErrorCannotFindHostNSURLErrorCannotFindHost | The connection failed because the host could not be found. | 
| -1004 kCFURLErrorCannotConnectToHostNSURLErrorCannotConnectToHost | The connection failed because a connection cannot be made to the host. | 
| -1005 kCFURLErrorNetworkConnectionLostNSURLErrorNetworkConnectionLost | The connection failed because the network connection was lost. | 
| -1006 kCFURLErrorDNSLookupFailedNSURLErrorDNSLookupFailed | The connection failed because the DNS lookup failed. | 
| -1007 kCFURLErrorHTTPTooManyRedirectsNSURLErrorHTTPTooManyRedirects | The HTTP connection failed due to too many redirects. | 
| -1008 kCFURLErrorResourceUnavailableNSURLErrorResourceUnavailable | The connection’s resource is unavailable. | 
| -1009 kCFURLErrorNotConnectedToInternetNSURLErrorNotConnectedToInternet | The connection failed because the device is not connected to the internet. | 
| -1010 kCFURLErrorRedirectToNonExistentLocationNSURLErrorRedirectToNonExistentLocation | The connection was redirected to a nonexistent location. | 
| -1011 kCFURLErrorBadServerResponseNSURLErrorBadServerResponse | The connection received an invalid server response. | 
| -1012 kCFURLErrorUserCancelledAuthenticationNSURLErrorUserCancelledAuthentication | The connection failed because the user cancelled required authentication. | 
| -1013 kCFURLErrorUserAuthenticationRequiredNSURLErrorUserAuthenticationRequired | The connection failed because authentication is required. | 
| -1014 kCFURLErrorZeroByteResourceNSURLErrorZeroByteResource | The resource retrieved by the connection is zero bytes. | 
| -1015 kCFURLErrorCannotDecodeRawDataNSURLErrorCannotDecodeRawData | The connection cannot decode data encoded with a known content encoding. | 
| -1016 kCFURLErrorCannotDecodeContentDataNSURLErrorCannotDecodeContentData | The connection cannot decode data encoded with an unknown content encoding. | 
| -1017 kCFURLErrorCannotParseResponseNSURLErrorCannotParseResponse | The connection cannot parse the server’s response. | 
| -1018 kCFURLErrorInternationalRoamingOff | The connection failed because international roaming is disabled on the device. | 
| -1019 kCFURLErrorCallIsActive | The connection failed because a call is active. | 
| -1020 kCFURLErrorDataNotAllowed | The connection failed because data use is currently not allowed on the device. | 
| -1021 kCFURLErrorRequestBodyStreamExhausted | The connection failed because its request’s body stream was exhausted. | 
| Code | Description | 
|---|---|
| -1100 kCFURLErrorFileDoesNotExistNSURLErrorFileDoesNotExist | The file operation failed because the file does not exist. | 
| -1101 kCFURLErrorFileIsDirectoryNSURLErrorFileIsDirectory | The file operation failed because the file is a directory. | 
| -1102 kCFURLErrorNoPermissionsToReadFileNSURLErrorNoPermissionsToReadFile | The file operation failed because it does not have permission to read the file. | 
| -1103 kCFURLErrorDataLengthExceedsMaximumNSURLErrorDataLengthExceedsMaximum | The file operation failed because the file is too large. | 
| Code | Description | 
|---|---|
| -1200 kCFURLErrorSecureConnectionFailedNSURLErrorSecureConnectionFailed | The secure connection failed for an unknown reason. | 
| -1201 kCFURLErrorServerCertificateHasBadDateNSURLErrorServerCertificateHasBadDate | The secure connection failed because the server’s certificate has an invalid date. | 
| -1202 kCFURLErrorServerCertificateUntrustedNSURLErrorServerCertificateUntrusted | The secure connection failed because the server’s certificate is not trusted. | 
| -1203 kCFURLErrorServerCertificateHasUnknownRootNSURLErrorServerCertificateHasUnknownRoot | The secure connection failed because the server’s certificate has an unknown root. | 
| -1204 kCFURLErrorServerCertificateNotYetValidNSURLErrorServerCertificateNotYetValid | The secure connection failed because the server’s certificate is not yet valid. | 
| -1205 kCFURLErrorClientCertificateRejectedNSURLErrorClientCertificateRejected | The secure connection failed because the client’s certificate was rejected. | 
| -1206 kCFURLErrorClientCertificateRequiredNSURLErrorClientCertificateRequired | The secure connection failed because the server requires a client certificate. | 
| Code | Description | 
|---|---|
| -2000 kCFURLErrorCannotLoadFromNetworkNSURLErrorCannotLoadFromNetwork | The connection failed because it is being required to return a cached resource, but one is not available. | 
| -3000 kCFURLErrorCannotCreateFileNSURLErrorCannotCreateFile | The file cannot be created. | 
| -3001 kCFURLErrorCannotOpenFileNSURLErrorCannotOpenFile | The file cannot be opened. | 
| -3002 kCFURLErrorCannotCloseFileNSURLErrorCannotCloseFile | The file cannot be closed. | 
| -3003 kCFURLErrorCannotWriteToFileNSURLErrorCannotWriteToFile | The file cannot be written. | 
| -3004 kCFURLErrorCannotRemoveFileNSURLErrorCannotRemoveFile | The file cannot be removed. | 
| -3005 kCFURLErrorCannotMoveFileNSURLErrorCannotMoveFile | The file cannot be moved. | 
| -3006 kCFURLErrorDownloadDecodingFailedMidStreamNSURLErrorDownloadDecodingFailedMidStream | The download failed because decoding of the downloaded data failed mid-stream. | 
| -3007 kCFURLErrorDownloadDecodingFailedToCompleteNSURLErrorDownloadDecodingFailedToComplete | The download failed because decoding of the downloaded data failed to complete. | 
| Code | Description | 
|---|---|
| -4000 kCFHTTPCookieCannotParseCookieFile | The cookie file cannot be parsed. | 
| Code | Description | 
|---|---|
| -72000L kCFNetServiceErrorUnknown | An unknown error occurred. | 
| -72001L kCFNetServiceErrorCollision | An attempt was made to use a name that is already in use. | 
| -72002L kCFNetServiceErrorNotFound | Not used. | 
| -72003L kCFNetServiceErrorInProgress | A new search could not be started because a search is already in progress. | 
| -72004L kCFNetServiceErrorBadArgument | A required argument was not provided or was not valid. | 
| -72005L kCFNetServiceErrorCancel | The search or service was cancelled. | 
| -72006L kCFNetServiceErrorInvalid | Invalid data was passed to a CFNetServices function. | 
| -72007L kCFNetServiceErrorTimeout | A search failed because it timed out. | 
| -73000L kCFNetServiceErrorDNSServiceFailure | An error from DNS discovery; look at kCFDNSServiceFailureKey to get the error number and interpret using dnssd.h | 
从这个巨大的表格头滚到尾,你可能期望看到一如继往的 NSHipster 哲学总结。这周没有。你知道编纂这个表格花了多长时间吗?就这样吧,NSRepetitiveStrainInjury 立在这里。
这是我处理错误的方式。
除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。