Асинхронная рекурсия закрытия для структур

Пишу сетевой клиент для Hacker News. Я использую их официальный API.

У меня возникли проблемы с модификацией моего сетевого клиента для работы со структурами вместо классов для комментариев к историям. Он отлично работает с классами, особенно с синхронным закрытием рекурсии.

Это моя модель данных.

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
    // ...
}

Переключение модели данных класса Comment на struct плохо работает с асинхронной рекурсией закрытия. Он отлично работает с классами, потому что на классы ссылаются, тогда как структуры копируются, и это вызывает некоторые проблемы.

Как я могу адаптировать свой сетевой клиент для работы со структурами? И есть ли способ переписать мои методы в один метод вместо двух? Один метод предназначен для комментариев верхнего уровня (корневых), а другой — рекурсия для ответов на каждый комментарий верхнего уровня (корневой).

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Рассмотрим этот блок кода

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, чтобы эта строка даже скомпилировалась), не копия в массиве.

Итак, вы можете сослаться на replies[replyIndex] в своем закрытии fetchReplies, например:

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)
    }
}

Спасибо за ваш ответ! Я пробовал ваши предложения, и это не работает. Я напечатал значение до и после этой строки replies[replyIndex].replies = replies. До печати nil и после печати Optional([nil])

user10711707 22.12.2020 17:53

Я также пробовал принудительно развернуть. replies[replyIndex]!.replies = replies Так было изначально replies[replyIndex]?.replies = replies

user10711707 22.12.2020 17:56

Да, придется развернуть.

Rob 22.12.2020 18:26

Кстати, все проблемы группы отправки, которые я указал в fetchReplies, относятся и к fetchComments.

Rob 22.12.2020 18:26

Большое спасибо Роб! Я настроил другие части своего приложения, и оно работает так, как ожидалось.

user10711707 22.12.2020 20:06

Другие вопросы по теме