结构的异步闭包递归

Asynchronous closure recursion for structs

我正在为 Hacker News 编写一个网络客户端。我正在使用他们的 official API.

我在修改我的网络客户端以使用结构而不是 classes 来处理故事评论时遇到问题。它适用于 classes,尤其是异步递归闭包。

这是我的数据模型。

class Comment: Item {
    var replies: [Comment?]?

    let id: Int
    let isDeleted: Bool?
    let parent: Int
    let repliesIDs: [Int]?
    let text: String?
    let time: Date
    let type: ItemType
    let username: String?

    enum CodingKeys: String, CodingKey {
        case isDeleted = "deleted"
        case id
        case parent
        case repliesIDs = "kids"
        case text
        case time
        case type
        case username = "by"
    }
}

这是我的网络客户端示例。

class NetworkClient {
    // ...
    // Top Level Comments
    func fetchComments(for story: Story, completionHandler: @escaping ([Comment]) -> Void) {
        var comments = [Comment?](repeating: nil, count: story.comments!.count)
        
        for (commentIndex, topLevelCommentID) in story.comments!.enumerated() {
            let topLevelCommentURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(topLevelCommentID).json")!
            
            dispatchGroup.enter()
            
            URLSession.shared.dataTask(with: topLevelCommentURL) { (data, urlResponse, error) in
                guard let data = data else {
                    print("Invalid top level comment data.")
                    return
                }
                
                do {
                    let comment = try self.jsonDecoder.decode(Comment.self, from: data)
                    comments[commentIndex] = comment
                    
                    if comment.repliesIDs != nil {
                        self.fetchReplies(for: comment) { replies in
                            comment.replies = replies
                        }
                    }
                    
                    self.dispatchGroup.leave()
                } catch {
                    print("There was a problem decoding top level comment JSON.")
                    print(error)
                    print(error.localizedDescription)
                }
            }.resume()
        }
        
        dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
            completionHandler(comments.compactMap { [=11=] })
        }
    }
    
    // Recursive method
    private func fetchReplies(for comment: Comment, completionHandler: @escaping ([Comment?]) -> Void) {
        var replies = [Comment?](repeating: nil, count: comment.repliesIDs!.count)
        
        for (replyIndex, replyID) in comment.repliesIDs!.enumerated() {
            let replyURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(replyID).json")!
            
            dispatchGroup.enter()
            
            URLSession.shared.dataTask(with: replyURL) { (data, _, _) in
                guard let data = data else { return }
                
                do {
                    let reply = try self.jsonDecoder.decode(Comment.self, from: data)
                    replies[replyIndex] = reply
                    
                    if reply.repliesIDs != nil {
                        self.fetchReplies(for: reply) { replies in
                            reply.replies = replies
                        }
                    }
                    
                    self.dispatchGroup.leave()
                } catch {
                    print(error)
                }
            }.resume()
        }
        
        dispatchGroup.notify(queue: .global(qos: .userInitiated)) {
            completionHandler(replies)
        }
    }
}

您可以像这样调用网络客户端来获取特定故事的评论树。

var comments = [Comment]()

let networkClient = NetworkClient()
networkClient.fetchStories(from: selectedStory) { commentTree in
    // ...
    comments = commentTree
    // ...
}

将 Comment class 数据模型转换为 struct 不能很好地与异步闭包递归配合使用。它适用于 classes,因为 classes 被引用而结构被复制并导致一些问题。

如何调整我的网络客户端以使用结构?有没有办法将我的方法重写为一个方法而不是两个?一种方法是针对顶级(根)评论,而另一种是针对每个顶级(根)评论回复的递归。

考虑这个代码块

let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply

if reply.repliesIDs != nil {
    self.fetchReplies(for: reply) { replies in
        reply.replies = replies
    }
}

如果 Comment 是一个结构,这将获取 reply,将它的副本添加到 replies 数组,然后,在 fetchReplies 中你正在改变原始 reply(您必须将其从 let 更改为 var 才能使该行甚至编译),而不是数组中的副本。

因此,您可能希望在 fetchReplies 闭包中引用 replies[replyIndex],例如:

let reply = try self.jsonDecoder.decode(Comment.self, from: data)
replies[replyIndex] = reply

if reply.repliesIDs != nil {
    self.fetchReplies(for: reply) { replies in
        replies[replyIndex].replies = replies
    }
}

顺便说一句,

  • 调度组不能是 属性,而必须是本地变量(尤其是当您似乎递归调用此方法时!);
  • 你有几个执行路径,你不会离开组(如果 datanil 或者如果 reply.repliesIDsnil 或者如果 JSON 解析失败);和
  • 您有过早离开组的执行路径(如果 reply.repliesIDs 不是 nil,您必须将 leave() 调用移动到该完成处理程序闭包中)。

我还没有测试过,但我会建议如下:

private func fetchReplies(for comment: Comment, completionHandler: @escaping ([Comment?]) -> Void) {
    var replies = [Comment?](repeating: nil, count: comment.repliesIDs!.count)
    let group = DispatchGroup() // local var
    
    for (replyIndex, replyID) in comment.repliesIDs!.enumerated() {
        let replyURL = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(replyID).json")!
        
        group.enter()
        
        URLSession.shared.dataTask(with: replyURL) { data, _, _ in
            guard let data = data else { 
                group.leave() // leave on failure, too
                return
            }
            
            do {
                let reply = try self.jsonDecoder.decode(Comment.self, from: data)
                replies[replyIndex] = reply
                
                if reply.repliesIDs != nil {
                    self.fetchReplies(for: reply) { replies in
                        replies[replyIndex].replies = replies
                        group.leave() // if reply.replieIDs was not nil, we must not `leave` until this is done
                    }
                } else {
                    group.leave() // leave if reply.repliesIDs was nil
                }
            } catch {
                group.leave() // leave on failure, too
                print(error)
            }
        }.resume()
    }
    
    dispatchGroup.notify(queue: .main) { // do this on main to avoid synchronization headaches
        completionHandler(replies)
    }
}