结构的异步闭包递归
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
}
}
顺便说一句,
- 调度组不能是 属性,而必须是本地变量(尤其是当您似乎递归调用此方法时!);
- 你有几个执行路径,你不会离开组(如果
data
是 nil
或者如果 reply.repliesIDs
是 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)
}
}
我正在为 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
}
}
顺便说一句,
- 调度组不能是 属性,而必须是本地变量(尤其是当您似乎递归调用此方法时!);
- 你有几个执行路径,你不会离开组(如果
data
是nil
或者如果reply.repliesIDs
是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)
}
}