NSErrorMattt Thompson Ricky Tan 🚩🌱

是个人就会犯错。 用 Cocoa 就会用到 NSError

Unix 系统中所有的程序都是其他进程的子进程,从原始进程一路分支(fork)出来,即那个不为所动的发起者:pid 1 (OS X 系统中是 launchd)。当可执行文件结束时,它用一个 0255 状态码与父进程通信,告诉它程序为什么或怎么退出的。0 意味着“一切正常退出;没有可通告的”,非零值表示发生了父进程需要注意的事。退出状态码可以用来表示进程是否崩溃,或者提前终止。按照惯例,越大的值,引发的错误越严重。

在面向对象编程范式中,大部分进程被抽象出来,只剩下对象及它们之间传递的消息。但是成功还是失败(以及不同种类的失败)在面向对象编程中仍然十分有用。但是考虑到方法通常不会返回非 BOOL 值,这就要创造一个新的东西还解决这个困境了。

那些比 Objective-C 更小题大做更好战的语言通过滥用异常来调解这个问题,哪怕一点点的不合规也抛异常。不过作为果粉幸运的是,当有坏事发生时 Objective-C 使用一种更文雅的方式,那便是 NSError


NSError 是基础框架中的无名英雄。勇敢地传入传出危险的方法调用,通过消息发送者我们就可以从所关联的上下文中找出我们的失败。当涉及到这样的问题时肯定没什么好事,但是传入 nilNSError ** 参数中并没有什么卵用。

NSErrorCFError 的无缝转换对象,但是你没理由会去深挖它在 Core Foundation 中对应的部分。

每个 NSError 对象编码了三部分关键的信息:状态码 code,对应的特定错误域 domain,还有额外的通过 userInfo 字典提供的上下文。

codedomain

与退出状态码不同的是,NSError -code 表示问题的本质。这些状态码都在一个特定的错误域 domain 中定义,以防重叠和混淆。这些状态码一般用 enum 来定义。

例如,在 NSCocoaErrorDomain 中,由 NSFileManager 访问一个不存在的文件产生的错误状态码是 4,正如 NSFileNoSuchFileError 所定义的。然而,4NSPOSIXErrorDomain 中指代 POSIX EINTR,或者“中断函数”错误

如今,任何有系统编程背影的人也许会只用一个 switch 语句加上少量的 printf 把数字常量翻译为人类可以阅读的信息。NSError 永远领先于你。

userInfo

是什么给了 NSError 它独特的魅力?正是这个每个人都喜欢的大杂烩属性:userInfo。作为整个 Cocoa 的惯例,userInfo 是一个可以包含任意键值对的字典,无论是为了继承或降低耦合的目的, 它都不适合拿来填满各种杂七杂八的属性。在 NSError 这个例子中,有一些特定的键值对应着只读属性。

这三个通常很有用:

而另外三个只用在 OS X:

以下是如何用 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 比特的返回类型 . 它不是 本地化的描述,但至少比每次去找一个交叉引用的错误码表要好!

为了完整性:以下是标准 NSErroruserInfo 的键列表:

使用 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;
}

NSURLErrorDomainCFNetworkErrors

iOS 应用中最大的失败来源是网络。在无线信号,信号传输,数据漫游,代理,安全性,认证以及各种协议协商中,每个过程都可能出错。

好的一面是,Foundation 中的 URL Loading 系统相当成熟,它接管了大部分事情。唯一不足的是不同的出错信息的文档分散在不同的编程指南和头文件中。如果你收到一个 -1004 的失败信息,你很难找出它是什么意思。

正因如此,这里有一份详尽的、格式化的表格供你查阅:

CodeDescription
-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.

Query the kCFSOCKSVersionKey to find the version requested by the server.

SOCKS4 错误

CodeDescription
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.

SOCKS5 错误

CodeDescription
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.

FTP 错误

CodeDescription
200
kCFFTPErrorUnexpectedStatusCode
The server returned an unexpected status code. Query the kCFFTPStatusCodeKey to get the status code returned by the server

HTTP 错误

CodeDescription
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.

CFURLConnection 及 CFURLProtocol 错误

CodeDescription
-998
kCFURLErrorUnknown
An unknown error occurred.
-999
kCFURLErrorCancelled
NSURLErrorCancelled
The connection was cancelled.
-1000
kCFURLErrorBadURL
NSURLErrorBadURL
The connection failed due to a malformed URL.
-1001
kCFURLErrorTimedOut
NSURLErrorTimedOut
The connection timed out.
-1002
kCFURLErrorUnsupportedURL
NSURLErrorUnsupportedURL
The connection failed due to an unsupported URL scheme.
-1003
kCFURLErrorCannotFindHost
NSURLErrorCannotFindHost
The connection failed because the host could not be found.
-1004
kCFURLErrorCannotConnectToHost
NSURLErrorCannotConnectToHost
The connection failed because a connection cannot be made to the host.
-1005
kCFURLErrorNetworkConnectionLost
NSURLErrorNetworkConnectionLost
The connection failed because the network connection was lost.
-1006
kCFURLErrorDNSLookupFailed
NSURLErrorDNSLookupFailed
The connection failed because the DNS lookup failed.
-1007
kCFURLErrorHTTPTooManyRedirects
NSURLErrorHTTPTooManyRedirects
The HTTP connection failed due to too many redirects.
-1008
kCFURLErrorResourceUnavailable
NSURLErrorResourceUnavailable
The connection’s resource is unavailable.
-1009
kCFURLErrorNotConnectedToInternet
NSURLErrorNotConnectedToInternet
The connection failed because the device is not connected to the internet.
-1010
kCFURLErrorRedirectToNonExistentLocation
NSURLErrorRedirectToNonExistentLocation
The connection was redirected to a nonexistent location.
-1011
kCFURLErrorBadServerResponse
NSURLErrorBadServerResponse
The connection received an invalid server response.
-1012
kCFURLErrorUserCancelledAuthentication
NSURLErrorUserCancelledAuthentication
The connection failed because the user cancelled required authentication.
-1013
kCFURLErrorUserAuthenticationRequired
NSURLErrorUserAuthenticationRequired
The connection failed because authentication is required.
-1014
kCFURLErrorZeroByteResource
NSURLErrorZeroByteResource
The resource retrieved by the connection is zero bytes.
-1015
kCFURLErrorCannotDecodeRawData
NSURLErrorCannotDecodeRawData
The connection cannot decode data encoded with a known content encoding.
-1016
kCFURLErrorCannotDecodeContentData
NSURLErrorCannotDecodeContentData
The connection cannot decode data encoded with an unknown content encoding.
-1017
kCFURLErrorCannotParseResponse
NSURLErrorCannotParseResponse
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.

文件错误

CodeDescription
-1100
kCFURLErrorFileDoesNotExist
NSURLErrorFileDoesNotExist
The file operation failed because the file does not exist.
-1101
kCFURLErrorFileIsDirectory
NSURLErrorFileIsDirectory
The file operation failed because the file is a directory.
-1102
kCFURLErrorNoPermissionsToReadFile
NSURLErrorNoPermissionsToReadFile
The file operation failed because it does not have permission to read the file.
-1103
kCFURLErrorDataLengthExceedsMaximum
NSURLErrorDataLengthExceedsMaximum
The file operation failed because the file is too large.

SSL 错误

CodeDescription
-1200
kCFURLErrorSecureConnectionFailed
NSURLErrorSecureConnectionFailed
The secure connection failed for an unknown reason.
-1201
kCFURLErrorServerCertificateHasBadDate
NSURLErrorServerCertificateHasBadDate
The secure connection failed because the server’s certificate has an invalid date.
-1202
kCFURLErrorServerCertificateUntrusted
NSURLErrorServerCertificateUntrusted
The secure connection failed because the server’s certificate is not trusted.
-1203
kCFURLErrorServerCertificateHasUnknownRoot
NSURLErrorServerCertificateHasUnknownRoot
The secure connection failed because the server’s certificate has an unknown root.
-1204
kCFURLErrorServerCertificateNotYetValid
NSURLErrorServerCertificateNotYetValid
The secure connection failed because the server’s certificate is not yet valid.
-1205
kCFURLErrorClientCertificateRejected
NSURLErrorClientCertificateRejected
The secure connection failed because the client’s certificate was rejected.
-1206
kCFURLErrorClientCertificateRequired
NSURLErrorClientCertificateRequired
The secure connection failed because the server requires a client certificate.

下载及文件 I/O 错误

CodeDescription
-2000
kCFURLErrorCannotLoadFromNetwork
NSURLErrorCannotLoadFromNetwork
The connection failed because it is being required to return a cached resource, but one is not available.
-3000
kCFURLErrorCannotCreateFile
NSURLErrorCannotCreateFile
The file cannot be created.
-3001
kCFURLErrorCannotOpenFile
NSURLErrorCannotOpenFile
The file cannot be opened.
-3002
kCFURLErrorCannotCloseFile
NSURLErrorCannotCloseFile
The file cannot be closed.
-3003
kCFURLErrorCannotWriteToFile
NSURLErrorCannotWriteToFile
The file cannot be written.
-3004
kCFURLErrorCannotRemoveFile
NSURLErrorCannotRemoveFile
The file cannot be removed.
-3005
kCFURLErrorCannotMoveFile
NSURLErrorCannotMoveFile
The file cannot be moved.
-3006
kCFURLErrorDownloadDecodingFailedMidStream
NSURLErrorDownloadDecodingFailedMidStream
The download failed because decoding of the downloaded data failed mid-stream.
-3007
kCFURLErrorDownloadDecodingFailedToComplete
NSURLErrorDownloadDecodingFailedToComplete
The download failed because decoding of the downloaded data failed to complete.
CodeDescription
-4000
kCFHTTPCookieCannotParseCookieFile
The cookie file cannot be parsed.

CFNetServices 错误

CodeDescription
-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 中国大陆」许可协议授权。