Прекратите выбрасывать ошибки

Мы все знаем, как это бывает, когда мы пытаемся заставить что-то работать. Мы хотим добиться результата как можно быстрее и сосредотачиваемся на «счастливом пути». Возможно, мы не так хорошо знакомы с технологией/языком/концепцией, но у нас есть одна конкретная цель или задача, которую мы пытаемся решить, и нас не так сильно волнуют побочные случаи или то, что произойдет, когда что-то пойдет не так.

На исследовательском этапе это обычно нормально. Может быть, вы пишете небольшой сценарий, чтобы решить какую-то проблему для себя, и никто больше не должен с этим сталкиваться. Может быть, вы просто пытаетесь понять, как что-то работает, и этот код в любом случае никогда не увидит свет. Какова бы ни была причина, вероятно, не стоит поначалу разбираться со всеми крайними случаями и ошибками.

Но я вижу, как многие начинающие разработчики iOS заходят в тупик, когда им кажется, что они написали код для счастливого пути, они запускают его, а он просто не работает.

У меня был профессор психологии, который говорил: «Когда люди не знают, что делать, они делают то, что знают, как делать». Он говорил это в контексте людей с различными зависимостями, но это верно для всех людей, и это еще один хороший пример. Когда начинающие разработчики заходят в тупик, они обращаются к знакомым им инструментам. Они добавляют операторы печати, устанавливают точки останова и пытаются определить, в чем проблема. Иногда это помогает. Это зависит от того, откуда исходит ошибка. Но есть много типов ошибок, в решении которых эти инструменты не особенно помогают. Одна из таких областей — декодирование.

Я потерял счет количеству новых разработчиков, которым я помогал разобраться с ошибками декодирования, и часто вижу одну и ту же ошибку. Они отбрасывают ошибки, которые помогли бы им отследить ошибку. В этой статье мы рассмотрим несколько ошибок декодирования. Они являются хорошим примером класса ошибок, в котором возникающие ошибки часто являются лучшим инструментом для понимания того, что пошло не так. Это также контекст, в котором довольно легко показать несколько конкретных примеров, поэтому он хорошо подходит для статьи такого размера. Но смысл верен везде. Ошибки и сообщения, которые они несут, существуют не просто так и часто являются полезным инструментом для поиска ошибок и повышения надежности вашего кода.

Так что не выбрасывайте их!

Давайте рассмотрим конкретный пример. Чтобы защитить эго различных людей, которым я помогал с расшифровкой ошибок на протяжении многих лет (по крайней мере, один из них сейчас работает в Apple), я сделал пример проекта, а не использовал чей-то реальный код. Но он иллюстрирует именно те проблемы, с которыми я сталкивался в реальном мире.

Обзор образца приложения

Мой пример приложения позволяет вам искать случайных персонажей из телешоу «Бургеры Боба». Вы можете нажать на кнопку, и приложение загрузит случайного персонажа, покажет вам его фотографию, имя, род занятий и кто его озвучивал. Также есть ссылка на фэндомную вики этого персонажа. Для получения информации используется api. Вот как она организована:

Есть модель персонажа:

struct Character: Codable {
    let id: Int
    let name: String
    let image: URL
    let occupation: String
    let voicedBy: String
    let wikiUrl: URL

    enum CodingKeys: String, CodingKey {
        case id, name, image, occupation
        case voicedBy = "voiced_by"
        case wikiUrl = "wiki_url"
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Есть класс, который инкапсулирует доступ к api с помощью этого интерфейса:

class BobsBurgersApi {
    static let shared = BobsBurgersApi()

    func fetchCharacter(id: Int) async -> Character? { ... }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Существует модель представления, которая взаимодействует с api и предоставляет примитивные строительные блоки для отображения представления:

@MainActor
class CharacterViewModel: ObservableObject {

    @Published var character: Character?

    var title: String { character?.name ?? "" }
    var subtitle: String { character?.occupation ?? "" }
    var detail: String { (character?.voicedBy).map { "Voiced by: ($0)" } ?? "" }
    var learnMore: URL { character?.wikiUrl ?? URL(string: "https://bobs-burgers.fandom.com")! }

    private var api: BobsBurgersApi { .shared }

    func changeCharacter() {
        Task {
            character = await api.fetchCharacter(id: .random(in: 1...501))
        }
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

И, наконец, есть представление SwiftUI, которое отображает информацию:

struct CharacterView: View {
    @StateObject var viewModel = CharacterViewModel()

    var body: some View {
        VStack {
            AsyncImage(url: viewModel.character?.image) { phase in
                phase.image?.resizable()
            }
            .aspectRatio(contentMode: .fit)
            .frame(width: 300, height: 300, alignment: .center)
            Spacer()
            Text(viewModel.title)
                .font(.title)
            Text(viewModel.subtitle)
            Text(viewModel.detail)
                .font(.caption)
            if !viewModel.title.isEmpty {
                Link("Learn More", destination: viewModel.learnMore)
                    .font(.caption)
            }
            Button("New Character") {
                viewModel.changeCharacter()
            }.padding()
        }
        .multilineTextAlignment(.center)
        .padding()
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Я лишь немного знаком со SwiftUI и использовал эту возможность, чтобы немного изучить его, поэтому я уверен, что есть лучшие/более идиосинкразические/эффективные способы написать это представление, но это не суть важно для данной статьи. Любые эксперты по SwiftUI, не стесняйтесь, дайте мне знать, как я мог бы это улучшить. Если мне понравится больше, чем то, что я написал, я обновлю эту статью и укажу вашу заслугу (если вы этого захотите).

Исправление декодирования

Теперь, когда у вас есть общее представление о приложении в целом, давайте более подробно рассмотрим, как работает декодирование в настоящее время. Вот с чего мы начнем:

// in BobsBurgersApi
private static let baseUrl = URL(string: "https://bobsburgers-api.herokuapp.com")!

func fetchCharacter(id: Int) async -> Character? {
    let url = Self.baseUrl
        .appendingPathComponent("character")
        .appendingPathComponent("(id)")
    guard let (data, _) = try? await URLSession.shared.data(from: url) else {
        return nil
    }
    let character = try? JSONDecoder().decode(Character.self, from: data)
    return character
}
Войти в полноэкранный режим Выход из полноэкранного режима

Очень часто я встречаю код, который написан подобным образом, по крайней мере, вначале. Логика довольно проста.

  1. Он создает url, добавляя «character» и заданный id к базовому url api.
  2. Пытается получить данные из этого url
  3. Пытается создать Character из этих данных.
  4. Если попытка успешна, возвращается символ, иначе возвращается nil.

Сейчас мы обнаружим, что это всегда не удается загрузить символ, и мы не получаем никакой обратной связи. Поэтому, потянувшись к знакомым инструментам, мы можем добавить операторы печати, подобные этому:

func fetchCharacter(id: Int) async -> Character? {
    let url = Self.baseUrl
        .appendingPathComponent("character")
        .appendingPathComponent("(id)")
    guard let (data, _) = try? await URLSession.shared.data(from: url) else {
        print("Couldn't fetch data")
        return nil
    }
    let character = try? JSONDecoder().decode(Character.self, from: data)
    if character == nil { print("Couldn't decode character") }
    return character
}

// prints: Couldn't decode character
Войти в полноэкранный режим Выход из полноэкранного режима

Это немного помогает нам, потому что теперь мы видим, что именно декодирование дает сбой и не получает данные, но это ничего не говорит нам о причинах сбоя. Далее мы можем попробовать распечатать сами данные, используя что-то вроде этого:

if character == nil { print(String(data: data, encoding: .utf8)!) }

// prints: {"error":"Error while retreiving data with id 157 in route character."}
Войти в полноэкранный режим Выйти из полноэкранного режима

И снова это немного полезнее. И в данном случае этого достаточно, чтобы отследить проблему. api не возвращает модель персонажа. Это не проблема декодирования, а проблема либо с сервером, либо с запросом, который мы делаем. Обратившись к документации, я заметил, что маршрут на самом деле /characters/:id, а не character/:id. Поэтому мы обновляем наш код, чтобы использовать «символы», и теперь он печатает:

{"id":3,"name":"Adam","image":"https://bobsburgers-api.herokuapp.com/images/characters/3.jpg","gender":"Male","hairColor":"Brown","relatives":[{"name":"Unnamed wife"}],"firstEpisode":""Mr. Lonely Farts"","voicedBy":"Brian Huskey","url":"https://bobsburgers-api.herokuapp.com/characters/3","wikiUrl":"https://bobs-burgers.fandom.com/wiki/Adam"}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы получаем реальную модель персонажа, но декодирование по-прежнему не работает. В чем же проблема?

Мы могли бы просмотреть ключи и значения по одному и убедиться, что они соответствуют структуре Character, которую мы определили. Это может сработать в данном контексте, потому что это относительно небольшая модель, но это может стать очень громоздким, если вы получаете длинный список или большую/сложную модель. Лучший способ (и смысл всей этой статьи) — использовать ошибку, которую мы получаем, чтобы дать нам некоторые подсказки. Поэтому давайте изменим нашу функцию fetchCharacter, чтобы перехватывать эти ошибки вместо того, чтобы отбрасывать их и превращать все в опции. Теперь это выглядит следующим образом:

func fetchCharacter(id: Int) async -> Character? {
    let url = Self.baseUrl
        .appendingPathComponent("characters")
        .appendingPathComponent("(id)")
    do {
        let (data, _) = try await URLSession.shared.data(from: url)
        let character = try JSONDecoder().decode(Character.self, from: data)
        return character
    } catch {
        print(error)
        return nil
    }
}
// note that this code is basically the same amount of code as we had before
// so don't try to use concision as a reason throw away errors
Вход в полноэкранный режим Выход из полноэкранного режима

А при запуске он выводит следующее:

keyNotFound(CodingKeys(stringValue: "voiced_by", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "voiced_by", intValue: nil) ("voiced_by").", underlyingError: nil))
Вход в полноэкранный режим Выход из полноэкранного режима

Это может выглядеть пугающе (и это может быть причиной того, что многие начинающие разработчики отбрасывают эти ошибки), но это не так плохо, как может показаться, и это здесь, чтобы помочь. Ошибка keyNotFound, и если мы посмотрим чуть дальше, то увидим, что ключ, который он не смог найти, это "voiced_by". Поэтому если мы вернемся к документации и прокрутим вниз до «Схемы символов», то увидим, что ключ на самом деле называется "voicedBy". В то же время мы можем заметить, что ключ, который мы определили как "wiki_url", на самом деле "wikiUrl". (Если бы мы не заметили, ошибка сообщила бы нам об этом при следующем запуске).

Так что давайте исправим это и запустим заново. В этом случае мы можем просто избавиться от перечисления CodingKeys, потому что все имена наших свойств совпадают с ключами в JSON.

//enum CodingKeys: String, CodingKey {
//    case id, name, image, occupation
//    case voicedBy = "voiced_by"
//    case wikiUrl = "wiki_url"
//}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем повторно запустить программу и увидеть на экране несколько реальных символов!

Однако, если вы будете перебирать символы достаточно долго, то обнаружите, что некоторые из них не удается декодировать. Давайте посмотрим, о чем говорят ошибки:

keyNotFound(CodingKeys(stringValue: "voicedBy", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "voicedBy", intValue: nil) ("voicedBy").", underlyingError: nil))
Войти в полноэкранный режим Выйти из полноэкранного режима

Интересно. Это та же самая ошибка, что и раньше, только теперь мы знаем, что правильно указали имя ключа. Вернемся к документации! Внизу в разделе «Схема символов» мы видим:

Ключ Тип Описание
voicedBy строка / неопределено Актер озвучивания персонажа

Это способ этого api сообщить, что voicedBy будет String, если он есть, или будет undefined (или nil), если его нет. В Swift это фактически другой тип, который мы называем опциональной строкой (String?, или Optional<String>), поэтому давайте обновим нашу модель. Пока мы это делаем, давайте проверим, должно ли что-нибудь еще быть необязательным.

Единственное, что я вижу, это occupation, поэтому обновим и его. Теперь наша модель выглядит следующим образом:

struct Character: Codable {
    let id: Int
    let name: String
    let image: URL
    let occupation: String?
    let voicedBy: String?
    let wikiUrl: URL
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем перебирать символы Bob’s Burger сколько душе угодно, и декодирование никогда не даст сбоя. Но если это произойдет, мы все равно выведем ошибку в консоль и сможем быстро отследить проблему.

Вложенный пример

Я заметил в документации, что у некоторых персонажей есть массив relatives. Если у определенного персонажа есть известные родственники, которые также находятся в системе, он вернет их в массиве уменьшенной модели персонажа. Это кажется интересной информацией, поэтому давайте добавим ее в наше приложение.

Я назову эту версию модели Relative, чтобы было понятно, с чем мы имеем дело:

extension Character {
    struct Relative: Codable {
        let name: String
        let wikiUrl: URL
        let url: URL
    }
}

// in Character struct
let relatives: [Relative]?
Вход в полноэкранный режим Выход из полноэкранного режима

Затем, чтобы мы могли видеть родственников на экране, я добавлю новое свойство к модели представления и отображу его на представлении:

// in CharacterViewModel
var subtitle2: String {
    let relatives = character?.relatives?.map(.name).joined(separator: ", ")
    return relatives.map { "Relatives: ($0)" } ?? ""
}

// in CharacterView, after subtitle
if !viewModel.subtitle2.isEmpty {
    Text(viewModel.subtitle2)
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, перебирая персонажей, вы обнаружите, что у многих из них нет родственников, но те, что есть, будут отображаться в этом списке. Время от времени, однако, вы будете находить такие символы, которые не удастся декодировать, и будет выведено что-то вроде этой ошибки:

keyNotFound(CodingKeys(stringValue: "wikiUrl", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "relatives", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: "wikiUrl", intValue: nil) ("wikiUrl").", underlyingError: nil))
Войдите в полноэкранный режим Выйти из полноэкранного режима

Может потребоваться немного больше усилий, чтобы понять, о чем говорит эта ошибка. В ней говорится, что не удается найти wikiUrl, и у нас может возникнуть соблазн подумать, что это wikiUrl в нашей модели Character, потому что у нее есть такая модель. Но если мы продолжим читать, то увидим в DecodingError.Context, что он смотрит на ключ relatives (который, как мы знаем, является массивом), на 0-й элемент индекса (то есть первый элемент в массиве), и там он не может найти ключ wikiUrl. Это означает, что у первого родственника в нашем массиве нет wikiUrl.

Это интересно, потому что в документации сказано, что wikiUrl не является опциональным, но, по-видимому, это так. Я распечатал json для этого символа, и вот что он возвращает для relatives:

"relatives" : [
    {
        "name" : "Unnamed child"
    }
]
Войти в полноэкранный режим Выйти из полноэкранного режима

Очевидно, есть по крайней мере один Relative, у которого нет wikiUrl или url, что мы можем учесть в нашей модели, сделав их необязательными (или просто не декодируя их, поскольку мы их не используем).

struct Relative: Codable {
    let name: String
    let wikiUrl: URL?
    let url: URL?
}

struct Relative: Codable {
    let name: String
//    let wikiUrl: URL
//    let url: URL
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это хорошее напоминание о том, что документация — это хорошее место для начала, но она не всегда актуальна. Если у вас есть доступ к разработчику, который поддерживает бэкенд, было бы неплохо предупредить его о несоответствии, чтобы он мог либо привести логику бэкенда в соответствие со спецификацией, либо обновить документацию, чтобы она соответствовала реальной логике. Или, если это API с открытым исходным кодом, вы можете открыть проблему и/или внести свой вклад!

Из этого немного более сложного примера видно, что эти ошибки помогают добраться до корня проблемы, даже если проблема глубоко вложена в ваш JSON. Они могут быть не в самой человекочитаемой форме, но можно проделать путь вниз по ним и получить ценную информацию. И, как и в большинстве случаев, с практикой вы станете лучше и в конце концов сможете просмотреть их, чтобы получить все, что вам нужно.

Подведение итогов

Примеры в этом простом приложении могут показаться тривиальными, но они отражают тот тип отладки, с которым обычно приходится работать, когда вы пытаетесь запустить новый сетевой интерфейс. Я видел, как люди тратили много времени, пытаясь понять, что не так с их декодированием, и придумывали всевозможные странные и сложные способы, чтобы попытаться определить проблему. Но, как мы сегодня рассмотрели, ошибки, несмотря на то, что сначала они могут показаться пугающими и трудночитаемыми, часто полны полезной информации, которая точно указывает нам, в чем проблема.

Поэтому в следующий раз, когда у вас возникнет соблазн выбросить ошибку, не делайте этого!

Сообщите мне в комментариях свое мнение об ошибках и о том, когда/когда не стоит их использовать. Полный начальный проект находится в этой ветке, а полный готовый продукт — в этой ветке.

Скрипты сообщений

Не все ошибки так полезны для отладки вашего кода, как в контексте декодирования. Как отвертка очень полезна для закручивания шурупов, но менее полезна для забивания гвоздей, так и ошибки полезны для отслеживания и устранения одних проблем и менее полезны для других. Моим намерением здесь было просто указать на то, что они являются полезным инструментом, и что они часто упускаются из виду начинающими разработчиками. Частью роста разработчика является изучение доступных инструментов, и со временем вы поймете, какие из них будут полезны в различных ситуациях.

Еще один момент, который мы не рассмотрели в этой статье, — когда/как сообщать пользователю о возникновении ошибки. Это зависит от вашего содержания/контекста и немного от личного вкуса. Но вообще говоря, хорошей практикой является передача ошибок по цепочке, по крайней мере, до уровня, где находится ваша бизнес-логика. Так вы, по крайней мере, получите информацию и сможете принять решение о том, как обработать ошибку так, чтобы это имело смысл для вашего приложения.

Оцените статью
devanswers.ru
Добавить комментарий