可以收集Swift异步抛出任务组的结果,即使其中一个任务抛出异常。

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

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作为键,其值可以是整数Int或错误Error的字典。逻辑上,值的类型可以是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&lt;Int, Error&gt;. Thus, the resulting dictionary would be a [Int: Result&lt;Int, Error&gt;]:

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

func results(for array: [Int]) async -&gt; [Int: Result&lt;Int, Error&gt;] {
    await withTaskGroup(of: (Int, Result&lt;Int, Error&gt;).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 -&gt; [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-2.html
匿名

发表评论

匿名网友

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

确定