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