`as` 在 Swift 中的工作方式是否取决于上下文?

Does the way `as` works in Swift depends on context?

Apple 提供了以下示例in their documentation

do {
    try customThrow()
} catch MyError.specificError1 {
    print("Caught specific error #1")
} catch let error as MyError where error.code == .specificError2 {
    print("Caught specific error #2, ", error.localizedDescription)
    // Prints "Caught specific error #2. A customized error from MyErrorDomain."
} catch let error {
    fatalError("Some other error: \(error)")
}

为什么这条线有效:

} catch let error as MyError where error.code == .specificError2 {

然而下面一行的结果是 nil:

let e = error as? MyError

?

要么 error 可以转换为 MyError,要么不能,对吗?那么这两条线怎么会产生不同的结果呢? as 的行为是否因上下文而异并且在 catch 子句中有所不同?

更新

链接的文档是关于 Cocoa 个错误,这意味着 NSError 个对象。因此,要重现,您需要按照以下方式做一些事情:

extern NSErrorDomain const MyErrorDomain;
typedef NS_ERROR_ENUM(MyErrorDomain, MyError) {
    specificError1 = 0,
    specificError2 = 1
};

然后你在 Obj-C 中创建并传递错误,而不是在 Swift:

NSError * error = [NSError errorWithDomain:MyErrorDomain
    code:specificError1 userInfo:nil];

[someObj handleError:error];

并且在 Swift 你有:

func handleError ( error: NSError ) {
    if let e = error as? MyError {
        // Never happens, never true
    }
}

然而当抛出错误时:

- (BOOL)throwError:(NSError **)outError {
    *outError = [NSError errorWithDomain:MyErrorDomain
        code:specificError1 userInfo:nil];
    return NO;
}

然后陷入 Swift,根据 Apple 的文档,这应该可以正常工作。我想知道这是怎么回事。

更新 2

按照代码路径,错误曾经作为 CFErrorRef 传递,这要归功于免费桥接,但当错误转换回 NSError 时,它不会' Swift 看起来不一样了。

这是最初创建错误时的 Swift 调试器输出:

(lldb) p error
(NSError) $R0 = 0x0000000100606ad0 domain: "MyErrorDomain" - code: 0 {
  _userInfo = 0x00007fff80983090
}

这是它作为 CFErrorRef 转了一圈又变成 NSError 后的输出:

(lldb) p error
(NSError) $R1 = 0x0000000105505360 domain: "MyErrorDomain" - code: 0 {
  ObjectiveC.NSObject = {
    baseNSObject@0 = {
      isa = __SwiftNativeNSError
    }
    _reserved = 0x0000000000000000
    _code = 0
    _domain = 0x0000000100004230 "MyErrorDomain"
    _userInfo = 0x00007fff80983090
  }
}

如您所见,还是同样的错误。相同的域,相同的代码,但现在它不能再转换为 MyError.

“为什么这条线有效:

} catch let error as MyError where error.code == .specificError2 {

我假设是“工作”,为什么它会编译。

你正在看什么

“赶上 let error as MyError where error.code == .specificError2”是一个

表达式模式

其中 let error as MyError 部分是

值绑定模式let 模式,其中模式 是一个

as-patternpattern as type

所以,那个东西完全在Swift语法中定义并由编译器解决。

“catch MyError.specificError1”中的 MyError.specificError1 是一个

表达式模式

需要重载 Swift 标准库 ~= 运算符。

所以,现在给出

let error: Error = ...

如果模式在 catch 子句中匹配:

catch let error as MyError where error.code == .specificError2

恕我直言,声明

let e = error as? MyError

应该分配 e 一个 MyError 值。

在 OP:如果你看不到这个,请提供一个例子。

更新:

澄清幕后发生的事情:

例如在这个(人为的)代码片段中:

switch error {
    case ...
        return ...
    case URLError.notConnectedToInternet:
        return MyError.internal(error)
    default:
        return ...
}

反汇编代码提供了一些见解:

    0x102684999 <+217>: callq  0x1026fb79a  
        ; symbol stub for: static Foundation
          .URLError
          .notConnectedToInternet
          .getter :
          Foundation.URLError.Code
    0x10268499e <+222>: movq   -0x48(%rbp), %rsi
    0x1026849a2 <+226>: movq   -0x40(%rbp), %rcx
    0x1026849a6 <+230>: movq   -0x38(%rbp), %rdi
    0x1026849aa <+234>: movq   %r13, %rdx
    0x1026849ad <+237>: callq  0x1026fb770
        ; symbol stub for: static Foundation.
          _ErrorCodeProtocol.~= infix(τ_0_0, Swift.Error) 
             -> Swift.Bool
    0x1026849b2 <+242>: movq   -0x38(%rbp), %rdi
    0x1026849b6 <+246>: movq   -0x30(%rbp), %rsi
    0x1026849ba <+250>: movb   %al, %cl
    0x1026849bc <+252>: movq   -0x20(%rbp), %rax
    0x1026849c0 <+256>: movb   %cl, -0x51(%rbp)
    0x1026849c3 <+259>: callq  *%rax
    0x1026849c5 <+261>: movb   -0x51(%rbp), %al
    0x1026849c8 <+264>: testb  [=12=]x1, %al
    0x1026849ca <+266>: jne    0x1026849ce               ; <+270> at HTTPClient.swift

这意味着,对于系统定义的 NSErrors(即使不是全部,也有很多) catch 子句中的模式表达式

catch SomeSystemError.specificError1

解决了为 Code 属性 类型调用中缀 ~= 运算符的问题,该类型在 Foundation 中定义 - 如果此 Code 类型符合 _ErrorCodeProtocol .

注意,中缀运算符 ~= 将在执行模式匹配时由 Swift 编译器调用。

更新 2

给定以下片段:

            } catch let err as URLError where err.code == URLError.notConnectedToInternet {
                return Error.internal(error)

let e = error as? URLError

反汇编代码在两个代码片段中都调用了 swift_dynamicCast。因此,表达式结果应该没有差异 IFF 作为参数给出的类型(此处 URLError)用于动态转换调用是相同的(我认为在 OPs 示例中给出)。

好吧,问题是 Apple 对 NSError 施展了某种魔法,这种魔法在以下过程中丢失了:

ObjC -> C -> Swift -> ObjC -> Swift

显然并非所有 NSError 都与 Swift 相同,即使它们与 Obj-C 相同。

要通过 C 传递该错误,它必须是 CFErrorRef。要将错误返回到 Obj-C,它必须再次是 NSError,但是 Swift 代码将错误检索为 Unmanaged<CFError>?.

将错误转换回来的代码执行了以下操作:

if let unmanagedError = errorRaw {
    let error = unmanagedError.takeRetainedValue() as Error as NSError
    if error.domain == "..." {

仅对于特定域,它需要转发代码 Obj-C,因此双重转换,因为不允许直接转换为 NSError。现在的问题是,如果该错误最终返回给 Swift 代码,则它不能再转换为 Swift 错误,尽管具有此类错误的正确域和代码。

实际有效的修复方法是将上面的代码修改如下:

let cfe = unmanagedError.takeRetainedValue()
let nse = cfe as Error as NSError
let error = NSError(domain: nse.domain,
    code: nse.code, userInfo: nse.userInfo);
if error.domain == "..." {

尽管这段代码看起来很愚蠢,但它确实对 Swift 错误是被强制转换还是重新创建产生了影响,而介于两者之间的 Obj-C 根本不会关心该代码完全没有区别。

总结

as 的行为不依赖于上下文,但它的行为不仅仅取决于类型。与 C/Obj-C 中的强制转换不同,“您可以始终将类型 A 强制转换为类型 B 或从不这样做”的规则不适用于 Swift,因为“as”动态强制转换并且如果强制转换是在运行时可能,并且不能不仅仅取决于源和目标类型。

NSError 的情况下,它还取决于 属性 值(domaincode)以及对象的内部表示。虽然第一个事实是有道理的,但第二个事实却没有,恕我直言,这是 Swift 运行时的一个实现怪癖,当使用 isEqual: 比较上面的两个不同错误时,它们实际上是并且对于相同的对象,相同的转换规则应该适用,因为相等意味着这些对象是“可互换的”,如果其中一个可以成功转换而另一个则不能,那么它们显然不是。