是否可以旋转关键字“抛出”?

is it possible to swizzle the keyword `throw`?

提问人:Tiny Tim 提问时间:2/18/2023 最后编辑:Ken WhiteTiny Tim 更新时间:2/19/2023 访问量:74

问:

我正在考虑错误处理,最近我学会了旋转。Swizzling 当然是一个不应该经常使用的工具,我想我理解这一点,但它让我想知道。如果每当抛出错误时,如果我想捕获抛出的错误。有没有办法使用swizzling或类似的东西来拦截错误并将其记录在某个地方而不会中断应用程序的流程?我正在考虑可能旋转关键字,但这可能不起作用。什么工具会用在这种事情上?throws

快速 的错误处理 投掷

评论

3赞 Alexander 2/18/2023
Swizzling 只能通过交换与 Objective C 运行时中的特定选择器相对应的方法实现来工作。任何不是方法调用的东西(不仅仅是任何方法调用,而是动态调度的方法调用,因此对 a 的调用)都不能被旋转。 不属于这一类。dynamic @objc functhrow

答:

3赞 Sweeper 2/18/2023 #1

不,你不能。许多编译器检查都依赖于“中断应用程序流”这一事实。例如,如果代码的某个路径,则该路径不需要:throwthrowreturn

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

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

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

另一个例子是:guard

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

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

关键是,这会将调用堆栈展开到可以捕获错误的点,并且不会执行依赖于没有错误的代码。throw

如果要以某种方式“捕获”错误,可以使用结果。第一个例子可以变成:

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)
    // now this will print "failure(...)"
    print(x)

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

您还可以使用 init(catching:) 将任何抛出调用包装到 .假设如果你不能改变,那么你可以改为这样做:Resultfoobar

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

第二个例子可以变成:

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

请注意,这仍然会“中断应用程序的流程”,因为如果为 nil,则仍未执行。你对此无能为力。printsomethingThatMayBeNil

您还可以添加一个方便的工厂方法来记录:

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

评论

0赞 Tiny Tim 2/18/2023
这真的得到了很好的解释。它似乎最常与转义闭包结合使用,并且在很大程度上被结构化并发所取代,我们现在有 .似乎继续使用 .谢谢扫地机!Resultasync throwsResult
1赞 rob mayoff 2/19/2023 #2

不,你不能摇晃.但是 Swift 运行时有一个钩子,_swift_WillThrow,它可以让你在它即将被抛出的那一刻检查它。这个钩子不是一个稳定的 API,可以在未来的 Swift 版本中更改或删除。throwError

如果您使用的是 Xcode 14.3(现在的 beta 版本)中包含的 Swift 5.8,则可以使用 _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"))")
    }
}