Swift 结构的异步闭包递归

Swift 结构的异步闭包递归,swift,asynchronous,recursion,network-programming,closures,Swift,Asynchronous,Recursion,Network Programming,Closures,我正在为黑客新闻写一个网络客户端。我在用他们的 我在修改网络客户端以使用结构而不是用于故事注释的类时遇到问题。它可以很好地处理类,尤其是使用非循环递归闭包 这是我的数据模型 class Comment: Item { var replies: [Comment?]? let id: Int let isDeleted: Bool? let parent: Int let repliesIDs: [Int]? let text: String?

我正在为黑客新闻写一个网络客户端。我在用他们的

我在修改网络客户端以使用结构而不是用于故事注释的类时遇到问题。它可以很好地处理类,尤其是使用非循环递归闭包

这是我的数据模型

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 { $0 })
        }
    }
    
    // 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
    // ...
}
将注释类数据模型切换到struct与异步闭包递归不兼容。它可以很好地处理类,因为类是被引用的,而结构是被复制的,这会导致一些问题


如何调整我的网络客户端以使用structs?有没有办法把我的方法重写成一个方法而不是两个?一种方法用于顶层(根)注释,而另一种方法用于每个顶层(根)注释回复的递归。

考虑此代码块

let reply=try self.jsonDecoder.decode(Comment.self,from:data)
回复[回复索引]=回复
如果reply.repliesIDs!=零{
self.fetchrepries(for:reply){repries in
回复。回复=回复
}
}
如果
Comment
是一个结构,这将获取
reply
,将它的一个副本添加到
reples
数组中,然后在
fetchreples
中对原始
reply
进行变异(必须将其从
let
更改为
var
,才能编译此行),不是数组中的副本

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

let reply=try self.jsonDecoder.decode(Comment.self,from:data)
回复[回复索引]=回复
如果reply.repliesIDs!=零{
self.fetchrepries(for:reply){repries in
回复[replyIndex]。回复=回复
}
}

顺便说一下

  • 调度组不能是一个属性,而必须是一个本地变量(特别是当您似乎在递归调用此方法时!)
  • 您有几个不离开组的执行路径(如果
    data
    was
    nil
    或如果
    reply.replesId
    was
    nil
    或如果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)
    }
}

谢谢你的回复!我试过你的建议,但没用。我打印了这一行的前后值
reply[replyIndex]。repries=repries
。打印前
nil
和打印后
可选([nil])
我也尝试了强制展开<代码>回复[replyIndex]!。replies=replies最初是这样的
replies[replyIndex]?。replies=replies
是的,你必须打开它。顺便说一下,我在
fetchReplies
中指出的所有调度组问题也适用于
fetchComments
。非常感谢Rob!我已经调整了我的应用程序的其他部分,它正在按预期工作。