是个人就会犯错。 用 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
的键列表:
NSLocalizedDescriptionKey
NSLocalizedFailureReasonErrorKey
NSLocalizedRecoverySuggestionErrorKey
NSLocalizedRecoveryOptionsErrorKey
NSFilePathErrorKey
NSStringEncodingErrorKey
NSUnderlyingErrorKey
NSRecoveryAttempterErrorKey
NSHelpAnchorErrorKey
NSError
你会在两种情况下遇到 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
和 CFNetworkErrors
iOS 应用中最大的失败来源是网络。在无线信号,信号传输,数据漫游,代理,安全性,认证以及各种协议协商中,每个过程都可能出错。
好的一面是,Foundation 中的 URL Loading 系统相当成熟,它接管了大部分事情。唯一不足的是不同的出错信息的文档分散在不同的编程指南和头文件中。如果你收到一个 -1004
的失败信息,你很难找出它是什么意思。
正因如此,这里有一份详尽的、格式化的表格供你查阅:
Code | Description |
---|---|
-1NSURLErrorUnknown | |
1kCFHostErrorHostNotFound | Indicates that the DNS lookup failed. |
2kCFHostErrorUnknown | 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 |
100kCFSOCKSErrorUnknownClientVersion | 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. |
101kCFSOCKSErrorUnsupportedServerVersion | The version of SOCKS requested by the server is not supported. Query kCFSOCKSStatusCodeKey to recover the status code returned by the server. |
Code | Description |
---|---|
110kCFSOCKS4ErrorRequestFailed | Request rejected or failed by the server. |
111kCFSOCKS4ErrorIdentdFailed | Request rejected because SOCKS server cannot connect to identd on the client. |
112kCFSOCKS4ErrorIdConflict | Request rejected because the client program and identd report different user-ids. |
113kCFSOCKS4ErrorUnknownStatusCode | The status code returned by the server is unknown. |
Code | Description |
---|---|
120kCFSOCKS5ErrorBadState | The stream is not in a state that allows the requested operation. |
121kCFSOCKS5ErrorBadResponseAddr | The address type returned is not supported. |
122kCFSOCKS5ErrorBadCredentials | The SOCKS server refused the client connection because of bad login credentials. |
123kCFSOCKS5ErrorUnsupportedNegotiationMethod | The requested method is not supported. Query kCFSOCKSNegotiationMethodKey to find the method requested. |
124kCFSOCKS5ErrorNoAcceptableMethod | The client and server could not find a mutually agreeable authentication method. |
Code | Description |
---|---|
200kCFFTPErrorUnexpectedStatusCode | The server returned an unexpected status code. Query the kCFFTPStatusCodeKey to get the status code returned by the server |
Code | Description |
---|---|
300kCFErrorHTTPAuthenticationTypeUnsupported | The client and server could not agree on a supported authentication type. |
301kCFErrorHTTPBadCredentials | The credentials provided for an authenticated connection were rejected by the server. |
302kCFErrorHTTPConnectionLost | The connection to the server was dropped. This usually indicates a highly overloaded server. |
303kCFErrorHTTPParseFailure | The HTTP server response could not be parsed. |
304kCFErrorHTTPRedirectionLoopDetected | Too many HTTP redirects occurred before reaching a page that did not redirect the client to another page. This usually indicates a redirect loop. |
305kCFErrorHTTPBadURL | The requested URL could not be retrieved. |
306kCFErrorHTTPProxyConnectionFailure | A connection could not be established to the HTTP proxy. |
307kCFErrorHTTPBadProxyCredentials | The authentication credentials provided for logging into the proxy were rejected. |
308kCFErrorPACFileError | An error occurred with the proxy autoconfiguration file. |
309kCFErrorPACFileAuth | The authentication credentials provided by the proxy autoconfiguration file were rejected. |
310kCFErrorHTTPSProxyConnectionFailure | A connection could not be established to the HTTPS proxy. |
311kCFStreamErrorHTTPSProxyFailureUnexpectedResponseToCONNECTMethod | The HTTPS proxy returned an unexpected status code, such as a 3xx redirect. |
Code | Description |
---|---|
-998kCFURLErrorUnknown | An unknown error occurred. |
-999kCFURLErrorCancelled NSURLErrorCancelled | The connection was cancelled. |
-1000kCFURLErrorBadURL NSURLErrorBadURL | The connection failed due to a malformed URL. |
-1001kCFURLErrorTimedOut NSURLErrorTimedOut | The connection timed out. |
-1002kCFURLErrorUnsupportedURL NSURLErrorUnsupportedURL | The connection failed due to an unsupported URL scheme. |
-1003kCFURLErrorCannotFindHost NSURLErrorCannotFindHost | The connection failed because the host could not be found. |
-1004kCFURLErrorCannotConnectToHost NSURLErrorCannotConnectToHost | The connection failed because a connection cannot be made to the host. |
-1005kCFURLErrorNetworkConnectionLost NSURLErrorNetworkConnectionLost | The connection failed because the network connection was lost. |
-1006kCFURLErrorDNSLookupFailed NSURLErrorDNSLookupFailed | The connection failed because the DNS lookup failed. |
-1007kCFURLErrorHTTPTooManyRedirects NSURLErrorHTTPTooManyRedirects | The HTTP connection failed due to too many redirects. |
-1008kCFURLErrorResourceUnavailable NSURLErrorResourceUnavailable | The connection’s resource is unavailable. |
-1009kCFURLErrorNotConnectedToInternet NSURLErrorNotConnectedToInternet | The connection failed because the device is not connected to the internet. |
-1010kCFURLErrorRedirectToNonExistentLocation NSURLErrorRedirectToNonExistentLocation | The connection was redirected to a nonexistent location. |
-1011kCFURLErrorBadServerResponse NSURLErrorBadServerResponse | The connection received an invalid server response. |
-1012kCFURLErrorUserCancelledAuthentication NSURLErrorUserCancelledAuthentication | The connection failed because the user cancelled required authentication. |
-1013kCFURLErrorUserAuthenticationRequired NSURLErrorUserAuthenticationRequired | The connection failed because authentication is required. |
-1014kCFURLErrorZeroByteResource NSURLErrorZeroByteResource | The resource retrieved by the connection is zero bytes. |
-1015kCFURLErrorCannotDecodeRawData NSURLErrorCannotDecodeRawData | The connection cannot decode data encoded with a known content encoding. |
-1016kCFURLErrorCannotDecodeContentData NSURLErrorCannotDecodeContentData | The connection cannot decode data encoded with an unknown content encoding. |
-1017kCFURLErrorCannotParseResponse NSURLErrorCannotParseResponse | The connection cannot parse the server’s response. |
-1018kCFURLErrorInternationalRoamingOff | The connection failed because international roaming is disabled on the device. |
-1019kCFURLErrorCallIsActive | The connection failed because a call is active. |
-1020kCFURLErrorDataNotAllowed | The connection failed because data use is currently not allowed on the device. |
-1021kCFURLErrorRequestBodyStreamExhausted | The connection failed because its request’s body stream was exhausted. |
Code | Description |
---|---|
-1100kCFURLErrorFileDoesNotExist NSURLErrorFileDoesNotExist | The file operation failed because the file does not exist. |
-1101kCFURLErrorFileIsDirectory NSURLErrorFileIsDirectory | The file operation failed because the file is a directory. |
-1102kCFURLErrorNoPermissionsToReadFile NSURLErrorNoPermissionsToReadFile | The file operation failed because it does not have permission to read the file. |
-1103kCFURLErrorDataLengthExceedsMaximum NSURLErrorDataLengthExceedsMaximum | The file operation failed because the file is too large. |
Code | Description |
---|---|
-1200kCFURLErrorSecureConnectionFailed NSURLErrorSecureConnectionFailed | The secure connection failed for an unknown reason. |
-1201kCFURLErrorServerCertificateHasBadDate NSURLErrorServerCertificateHasBadDate | The secure connection failed because the server’s certificate has an invalid date. |
-1202kCFURLErrorServerCertificateUntrusted NSURLErrorServerCertificateUntrusted | The secure connection failed because the server’s certificate is not trusted. |
-1203kCFURLErrorServerCertificateHasUnknownRoot NSURLErrorServerCertificateHasUnknownRoot | The secure connection failed because the server’s certificate has an unknown root. |
-1204kCFURLErrorServerCertificateNotYetValid NSURLErrorServerCertificateNotYetValid | The secure connection failed because the server’s certificate is not yet valid. |
-1205kCFURLErrorClientCertificateRejected NSURLErrorClientCertificateRejected | The secure connection failed because the client’s certificate was rejected. |
-1206kCFURLErrorClientCertificateRequired NSURLErrorClientCertificateRequired | The secure connection failed because the server requires a client certificate. |
Code | Description |
---|---|
-2000kCFURLErrorCannotLoadFromNetwork NSURLErrorCannotLoadFromNetwork | The connection failed because it is being required to return a cached resource, but one is not available. |
-3000kCFURLErrorCannotCreateFile NSURLErrorCannotCreateFile | The file cannot be created. |
-3001kCFURLErrorCannotOpenFile NSURLErrorCannotOpenFile | The file cannot be opened. |
-3002kCFURLErrorCannotCloseFile NSURLErrorCannotCloseFile | The file cannot be closed. |
-3003kCFURLErrorCannotWriteToFile NSURLErrorCannotWriteToFile | The file cannot be written. |
-3004kCFURLErrorCannotRemoveFile NSURLErrorCannotRemoveFile | The file cannot be removed. |
-3005kCFURLErrorCannotMoveFile NSURLErrorCannotMoveFile | The file cannot be moved. |
-3006kCFURLErrorDownloadDecodingFailedMidStream NSURLErrorDownloadDecodingFailedMidStream | The download failed because decoding of the downloaded data failed mid-stream. |
-3007kCFURLErrorDownloadDecodingFailedToComplete NSURLErrorDownloadDecodingFailedToComplete | The download failed because decoding of the downloaded data failed to complete. |
Code | Description |
---|---|
-4000kCFHTTPCookieCannotParseCookieFile | The cookie file cannot be parsed. |
Code | Description |
---|---|
-72000LkCFNetServiceErrorUnknown | An unknown error occurred. |
-72001LkCFNetServiceErrorCollision | An attempt was made to use a name that is already in use. |
-72002LkCFNetServiceErrorNotFound | Not used. |
-72003LkCFNetServiceErrorInProgress | A new search could not be started because a search is already in progress. |
-72004LkCFNetServiceErrorBadArgument | A required argument was not provided or was not valid. |
-72005LkCFNetServiceErrorCancel | The search or service was cancelled. |
-72006LkCFNetServiceErrorInvalid | Invalid data was passed to a CFNetServices function. |
-72007LkCFNetServiceErrorTimeout | A search failed because it timed out. |
-73000LkCFNetServiceErrorDNSServiceFailure | An error from DNS discovery; look at kCFDNSServiceFailureKey to get the error number and interpret using dnssd.h |
从这个巨大的表格头滚到尾,你可能期望看到一如继往的 NSHipster 哲学总结。这周没有。你知道编纂这个表格花了多长时间吗?就这样吧,NSRepetitiveStrainInjury
立在这里。
这是我处理错误的方式。
除非另有声明,本文采用知识共享「署名-非商业性使用 3.0 中国大陆」许可协议授权。