是否可以交换关键字`throw`?

huangapple go评论72阅读模式
英文:

is it possible to swizzle the keyword `throw`?

问题

我在考虑错误处理,最近学到了关于“swizzling”的知识。Swizzling 显然是一个不应该经常使用的工具,我理解了这一点,但它让我想到了一个问题。每当抛出一个错误时,如果我想捕获这个错误,有没有办法可以使用 swizzling 或类似的方法来拦截错误并将其记录在某个地方,而不会中断应用程序的流程?我在考虑可能会对 throws 关键字进行 swizzling,但这可能行不通。用于这种情况的工具有哪些?

英文:

I was thinking about error handling and I learned about swizzling recently. Swizzling is certainly a tool which shouldn't be used too often, and I think I understand that, but it made me wonder. If whenever an error is thrown, if I wanted to capture the thrown error. Is there a way I could use swizzling or some such in order to intercept the error and log it somewhere without interrupting the flow of the app? I was thinking about possibly swizzling the throws keyword, but that might not work. What tools would be used for this kind of thing?

答案1

得分: 3

你不能这样做。许多编译器检查 依赖于 throw “中断应用程序流程”的事实。例如,如果代码的某个路径 throw 了,那么该路径就不需要 return

func foo(x: Bool) throws -> Int {
    if x {
        throw someError
    } else {
        return 1
    }
}

现在如果 throw someError 没有“中断应用程序流程”,在以下代码中 bar 会打印出什么?

func bar() throws {
    let x = try foo(x: true)
    print(x)
}

另一个例子是 guard

guard let value = somethingThatMayBeNil else {
    throw someError
}
print(value.nowICanSafelyAccessThis)

如果上面的 throw someError 实际上没有“中断应用程序流程”,并且停止运行方法的其余部分,那么在 print(value.nowICanSafelyAccessThis) 处将会发生非常糟糕的事情,因为 somethingThatMayBeNil 是空的,我甚至不确定 value 是否存在。

重点是,throw 将会展开调用堆栈,直到可以捕获错误的地方,并且依赖于没有错误发生的代码不会执行。

如果你想以某种方式“捕获”错误,你可以使用 Result。第一个例子可以改写成:

func foo(x: Bool) -> Result<Int, Error> {
    if x {
        return Result.failure(someError)
    } else {
        return Result.success(1)
    }
}

func bar() {
    let x = foo(x: true)
    // 现在这将会打印出 "failure(...)"
    print(x)

    // 而 x 的类型是 Result<Int, Error>,而不是 Int
    switch x {
    case .failure(let e):
        // 在这里记录错误 e...
    case .success(let theInt):
        // 使用 theInt 做一些事情...
    }
}

你也可以使用 init(catching:) 将任何可能抛出错误的调用包装成一个 Result。假设如果你不能改变 foo,那么你可以在 bar 中这样做:

func bar() {
    let x = Result { try foo(x: true) }
    ...
}

第二个例子可以改写成:

guard let value = somethingThatMayBeNil else {
    // 假设返回类型已经相应地更改
    return Result.failure(someError)
}
print(value.nowICanSafelyAccessThis)

请注意,这仍然会“中断应用程序流程”,也就是说如果 somethingThatMayBeNil 是空的,print 将不会执行。你无法改变这一点。

你也可以为日志记录添加一个方便的工厂方法:

extension Result {
    static func failureAndLog(_ error: Failure) -> Result {
        // 在这里做日志记录...
        
        return .failure(error)
    }
}
英文:

No you cannot. Many compiler checks depend on the fact that throw "interrupts the flow of the app". For example, if some path of the code throws, then that path doesn't need to return:

func foo(x: Bool) throws -&gt; Int {
    if x {
        throw someError
    } else {
        return 1
    }
}

Now if throw someError did not "interrupt the flow of the app", what would bar print in the following code?

func bar() throws {
    let x = try foo(x: true)
    print(x)
}

Another example is guard:

guard let value = somethingThatMayBeNil else {
    throw someError
}
print(value.nowICanSafelyAccessThis)

If throw someError above didn't actually "interrupt the flow of the app" and stop running the rest of the method, something really bad is going to happen at print(value.nowICanSafelyAccessThis), because somethingThatMayBeNil is nil, and I'm not even sure value even exists.

The whole point is that throw would unwind the call stack to a point where the error can be caught, and that the code that depends on there not being an error is not executed.

If you want to "capture" the error in some way, you can use a Result. The first example can be turned into:

func foo(x: Bool) -&gt; Result&lt;Int, Error&gt; {
    if x {
        return Result.failure(someError)
    } else {
        return Result.success(1)
    }
}

func bar() {
    let x = foo(x: true)
    // now this will print &quot;failure(...)&quot;
    print(x)

    // and x is of type Result&lt;Int, Error&gt;, rather than Int
    switch x {
    case .failure(let e):
        // log the error e here...
    case .success(let theInt):
        // do something with theInt...
    }
}

You can also use init(catching:) to wrap any throwing call into a Result. Suppose if you can't change foo, then you can do this in bar instead:

func bar() {
    let x = Result { try foo(x: true) }
    ...

The second example can be turned into:

guard let value = somethingThatMayBeNil else {
    // assuming the return type is changed appropriately
    return Result.failure(someError)
}
print(value.nowICanSafelyAccessThis)

Note that this will still "interrupt the flow of the app", as in the print is still not executed if somethingThatMayBeNil is nil. There is nothing you can do about that.

You could also add a convenient factory method for logging:

extension Result {
    static func failureAndLog(_ error: Failure) -&gt; Result {
        // do your logging here...
        
        return .failure(error)
    }
}

答案2

得分: 1

不可以对 throw 进行 swizzle。但是 Swift 运行时提供了一个钩子 _swift_WillThrow,允许你在 Error 被抛出前检查它。这个钩子不是一个稳定的 API,未来版本的 Swift 可能会进行更改或移除。

如果你使用的是 Swift 5.8(包含在 Xcode 14.3 中,目前是 beta 版本),你可以使用 _swift_setWillThrowHandler 函数来设置 _swift_willThrow 函数:

@_silgen_name("_swift_setWillThrowHandler")
func setWillThrowHandler(
    _ handler: (@convention(c) (UnsafeRawPointer) -> Void)?
)

var errors: [String] = []

func tryItOut() {
    setWillThrowHandler {
        let error = unsafeBitCast($0, to: Error.self)
        let callStack = Thread.callStackSymbols.joined(separator: "\n")
        errors.append("""
            \(error)
            \(callStack)
            """)
    }

    do {
        throw MyError()
    } catch {
        print("caught \(error)")
        print("errors = \(errors.joined(separator: "\n\n"))")
    }
}

输出:

caught MyError()
errors = MyError()
0   iPhoneStudy                         0x0000000102a97d9c $s11iPhoneStudy8tryItOutyyFySVcfU_ + 252
1   iPhoneStudy                         0x0000000102a97ff0 $s11iPhoneStudy8tryItOutyyFySVcfU_To + 12
2   libswiftCore.dylib                  0x000000018c2f4ee0 swift_willThrow + 56
3   iPhoneStudy                         0x0000000102a978f8 $s11iPhoneStudy8tryItOutyyF + 160
4   iPhoneStudy                         0x0000000102a99740 $s11iPhoneStudy6MyMainV4mainyyFZ + 28
5   iPhoneStudy                         0x0000000102a997d0 $s11iPhoneStudy6MyMainV5$mainyyFZ + 12
6   iPhoneStudy                         0x0000000102a99f48 main + 12
7   dyld                                0x0000000102d15514 start_sim + 20
8   ???                                 0x0000000102e11e50 0x0 + 4343275088
9   ???                                 0x9f43000000000000 0x0 + 11476016275470155776

如果你使用的是旧版本的 Swift(但至少是 Swift 5.2,我想是在 Xcode 11.4 中),你需要直接访问 _swift_willThrow 钩子:

var swift_willThrow: UnsafeMutablePointer<(@convention(c) (UnsafeRawPointer) -> Void)?> {
    get {
        dlsym(UnsafeMutableRawPointer(bitPattern: -2), "_swift_willThrow")!
            .assumingMemoryBound(to: (@convention(c) (UnsafeRawPointer) -> Void)?.self)
    }
}

var errors: [String] = []

func tryItOut() {
    swift_willThrow.pointee = {
        let error = unsafeBitCast($0, to: Error.self)
        let callStack = Thread.callStackSymbols.joined(separator: "\n")
        errors.append("""
            \(error)
            \(callStack)
            """)
    }

    do {
        throw MyError()
    } catch {
        print("caught \(error)")
        print("errors = \(errors.joined(separator: "\n\n"))")
    }
}
英文:

No, you can't swizzle throw. But the Swift runtime has a hook, _swift_WillThrow, that lets you examine an Error at the moment it's about to be thrown. This hook is not a stable API and could be changed or removed in future versions of Swift.

If you're using Swift 5.8, which is included in Xcode 14.3 (in beta release now), you can use the _swift_setWillThrowHandler function to set the _swift_willThrow function:

@_silgen_name(&quot;_swift_setWillThrowHandler&quot;)
func setWillThrowHandler(
    _ handler: (@convention(c) (UnsafeRawPointer) -&gt; Void)?
)

var errors: [String] = []

func tryItOut() {
    setWillThrowHandler {
        let error = unsafeBitCast($0, to: Error.self)
        let callStack = Thread.callStackSymbols.joined(separator: &quot;\n&quot;)
        errors.append(&quot;&quot;&quot;
            \(error)
            \(callStack)
            &quot;&quot;&quot;)
    }

    do {
        throw MyError()
    } catch {
        print(&quot;caught \(error)&quot;)
        print(&quot;errors = \(errors.joined(separator: &quot;\n\n&quot;))&quot;)
    }
}

Output:

caught MyError()
errors = MyError()
0   iPhoneStudy                         0x0000000102a97d9c $s11iPhoneStudy8tryItOutyyFySVcfU_ + 252
1   iPhoneStudy                         0x0000000102a97ff0 $s11iPhoneStudy8tryItOutyyFySVcfU_To + 12
2   libswiftCore.dylib                  0x000000018c2f4ee0 swift_willThrow + 56
3   iPhoneStudy                         0x0000000102a978f8 $s11iPhoneStudy8tryItOutyyF + 160
4   iPhoneStudy                         0x0000000102a99740 $s11iPhoneStudy6MyMainV4mainyyFZ + 28
5   iPhoneStudy                         0x0000000102a997d0 $s11iPhoneStudy6MyMainV5$mainyyFZ + 12
6   iPhoneStudy                         0x0000000102a99f48 main + 12
7   dyld                                0x0000000102d15514 start_sim + 20
8   ???                                 0x0000000102e11e50 0x0 + 4343275088
9   ???                                 0x9f43000000000000 0x0 + 11476016275470155776

If you're using an older Swift (but at least Swift 5.2 I think, which was in Xcode 11.4), you have to access the _swift_willThrow hook directly:

var swift_willThrow: UnsafeMutablePointer&lt;(@convention(c) (UnsafeRawPointer) -&gt; Void)?&gt; {
    get {
        dlsym(UnsafeMutableRawPointer(bitPattern: -2), &quot;_swift_willThrow&quot;)!
            .assumingMemoryBound(to: (@convention(c) (UnsafeRawPointer) -&gt; Void)?.self)
    }
}

var errors: [String] = []

func tryItOut() {
    swift_willThrow.pointee = {
        let error = unsafeBitCast($0, to: Error.self)
        let callStack = Thread.callStackSymbols.joined(separator: &quot;\n&quot;)
        errors.append(&quot;&quot;&quot;
            \(error)
            \(callStack)
            &quot;&quot;&quot;)
    }

    do {
        throw MyError()
    } catch {
        print(&quot;caught \(error)&quot;)
        print(&quot;errors = \(errors.joined(separator: &quot;\n\n&quot;))&quot;)
    }
}

huangapple
  • 本文由 发表于 2023年2月18日 07:49:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/75490214.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定