一个 Swift 的异步抛出任务组的结果能否在其中一个任务抛出异常时被收集?

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

Can a swift async throwing task groups results be collected even if one task throws?

问题

我正在尝试创建处理数组的代码。在这个问题中,我们假设这是一个整数数组。处理它们的异步函数可以抛出异常,也可以对它们进行一些操作并返回结果。

在创建任务组后,我遇到了一些问题,因为没有太多的方法可以优雅地处理异常。例如,使用.map()需要在整个代码块前面加上try,这意味着如果任何一个任务抛出异常,我将得不到任何结果。

在这种情况下,我预计有时会出现任务抛出异常的情况。如果某个任务抛出异常,我并不关心它。我只想丢弃这个结果,专注于那些没有抛出异常的结果。

我找到的唯一方法是使用nextResult(),它会将抛出的错误转换为可以正确处理的枚举类型。但是使用它生成的代码并不理想。是否有更优雅的编写代码的方式?

func asyncThrowOnOddNumbers(_ num: Int) async throws -> (Int, Int) {
    if num % 2 == 1 {
        throw NSError(domain: "OK", code: 1)
    }
    return (num, num * 10)
}

let numSet = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

let results = await withThrowingTaskGroup(of: (Int, Int).self) { group in
    for num in numSet {
        group.addTask {
            return try await asyncThrowOnOddNumbers(num)
        }
    }
    
    var successDir: [Int : Int] = [:]
    while let workResult = await group.nextResult() {
        switch workResult {
        case .success(let value): successDir[value.0] = value.1
        case .failure(_): continue
        }
    }
    
    return successDir
}
// 目标输出: "[2: 20, 6: 60, 10: 100, 8: 80, 4: 40]"
// 拓展目标: 获取包含错误或数字的数组 "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()...]"
英文:

I am trying to create code that processes an array. For the purposes of this question we will assume it is an array of integers. The async function that processes them can either throw or do some work on them and return the values.

I have been having issues collecting the results after I create the task group as there really aren't many methods that allow me to recover from throwing gracefully. .map() for example requires me to put try in front of the whole thing which means if any of the tasks throw I get out nothing.

In this case I expect on occasion a task will throw. If some task throws I really don't care about it. I would just like to throw away the result and focus on the ones that don't throw.

The only way I found to do this is to use nextResult() which thankfully converts the thrown error into an enum that can be handled properly. But the resulting code from using it isn't great. Is there a more eloquent way to write this code?

func asyncThrowOnOddNumbers(_ num: Int) async throws -> (Int, Int) {
    if num % 2 == 1 {
        throw NSError(domain: "OK", code: 1)
    }
    return (num, num * 10)
}

let numSet = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

let results = await withThrowingTaskGroup(of: (Int, Int).self) { group in
    for num in numSet {
        group.addTask {
            return try await asyncThrowOnOddNumbers(num)
        }
    }
    
    var successDir: [Int : Int] = [:]
    while let workResult = await group.nextResult() {
        switch workResult {
        case .success(let value): successDir[value.0] = value.1
        case .failure(_): continue
        }
    }
    
    return successDir
}
// GOAL print "[2: 20, 6: 60, 10: 100, 8: 80, 4: 40]"
// Stretch goal get array with error or a number "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()..."

答案1

得分: 2

也许你不应该使用ThrowingTaskGroup,而是使用一个非抛出异常的任务组。

将子任务的类型设为可选类型,以将失败的任务表示为nil。然后你可以使用try?而不是try来获取一个可选值。

let results = await withTaskGroup(of: (Int, Int)?.self) { group in
    for num in numSet {
        group.addTask {
            try? await asyncThrowOnOddNumbers(num)
        }
    }
    return await group
        .compactMap { $0 }
        .reduce(into: [:]) { acc, new in acc[new.0] = new.1 }
}

如果你想要同时获取错误信息,那么你使用nextResult的方法可能是最简洁的方式。

英文:

Perhaps you should not use a ThrowingTaskGroup, and use a non-throwing one instead.

Make the child task's type optional, to represent failed tasks as nil. Then you can use try? instead of try to get an optional.

let results = await withTaskGroup(of: (Int, Int)?.self) { group in
    for num in numSet {
        group.addTask {
            try? await asyncThrowOnOddNumbers(num)
        }
    }
    return await group
        .compactMap { $0 }
        .reduce(into: [:]) { acc, new in acc[new.0] = new.1 }
}

If you want the error as well, then your approach of using nextResult is probably the shortest way to do it.

答案2

得分: 1

你说:

> Stretch goal get array with error or a number "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()..."

所以,你想要一个以Int为键,值为IntError的字典。逻辑上,"value"的解决方案应该是Result<Int, Error>。因此,结果字典将是[Int: Result<Int, Error>]

func asyncThrowOnOddNumbers(_ num: Int) async throws -> Int {
    if num % 2 == 1 {
        throw NSError(domain: "OK", code: 1)
    }
    return num * 10
}

func results(for array: [Int]) async -> [Int: Result<Int, Error>] {
    await withTaskGroup(of: (Int, Result<Int, Error>).self) { group in
        for num in array {
            group.addTask {
                do {
                    return try await (num, .success(self.asyncThrowOnOddNumbers(num)))
                } catch {
                    return (num, .failure(error))
                }
            }
        }

        return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

(顺便说一句,我认为创建元组的责任不应该由被调用的函数承担,而是在withTaskGroup内部完成,所以我以上面的模式进行了处理。但无论哪种方式,希望你明白我的意思。)


顺便提一下,如果你只想要一个省略了抛出错误的[Int: Int],你可以这样做:

func results(for array: [Int]) async -> [Int: Int] {
    await withTaskGroup(of: (Int, Int)?.self) { group in
        for num in array {
            group.addTask {
                return try? await (num, self.asyncThrowOnOddNumbers(num))
            }
        }

        return await group
            .compactMap { $0 }
            .reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}
英文:

You said:

> Stretch goal get array with error or a number "[1: Error(), 2: 20, 3: Error(), 4: 40, 5: Error()..."

So, you want a dictionary keyed by an Int, whose value is either an Int or an Error. The logical solution for the “value” would be a Result<Int, Error>. Thus, the resulting dictionary would be a [Int: Result<Int, Error>]:

func asyncThrowOnOddNumbers(_ num: Int) async throws -> Int {
    if num % 2 == 1 {
        throw NSError(domain: "OK", code: 1)
    }
    return num * 10
}

func results(for array: [Int]) async -> [Int: Result<Int, Error>] {
    await withTaskGroup(of: (Int, Result<Int, Error>).self) { group in
        for num in array {
            group.addTask {
                do {
                    return try await (num, .success(self.asyncThrowOnOddNumbers(num)))
                } catch {
                    return (num, .failure(error))
                }
            }
        }

        return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

(As an aside, I don't think it is the responsibility of the called function to create the tuple, but rather do that stuff within the withTaskGroup, so that's the pattern I've employed above. But either way, hopefully you get the idea.)


FWIW, if you just wanted a [Int: Int] that omits the thrown errors, you can do:

func results(for array: [Int]) async -> [Int: Int] {
    await withTaskGroup(of: (Int, Int)?.self) { group in
        for num in array {
            group.addTask {
                return try? await (num, self.asyncThrowOnOddNumbers(num))
            }
        }

        return await group
            .compactMap { $0 }
            .reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

huangapple
  • 本文由 发表于 2023年8月9日 16:20:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76865849.html
匿名

发表评论

匿名网友

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

确定