停止丢弃错误
#调试 #ios #swift #xcode

我们都知道当我们试图使某些东西有效时。我们希望尽可能快地到达那里,并专注于“快乐的道路”。也许我们对技术/语言/概念并不熟悉,但是我们有一个特定的目标或任务,我们不太在乎边缘案例,或者当出现问题时会发生什么。 /p>

在探索阶段,这通常很好。也许您正在编写一些脚本来为自己解决一些问题,没有其他人必须处理它。也许您只是想弄清楚某事的工作原理,无论如何,这个代码永远不会看到一天的光芒。无论出于何种原因,首先不处理所有边缘案例和错误可能都可以。

但是,我看到很多较新的iOS开发人员被卡住了,那就是当他们认为他们有写的《快乐之路》的代码时,他们去运行它,而且它不起作用。

我有一位心理学教授,他曾经说过:“当人们不知道该怎么做时,他们会做他们知道如何做的事情”。他在有各种成瘾的人的背景下说了这句话,但所有人都是如此,这是另一个很好的例子。当较新的开发人员被困在这里时,他们将达到他们熟悉的工具。他们将添加打印语句并设置断点,并尝试缩小问题的位置。有时这会有所帮助。这取决于错误的来源。但是,这些工具有很多类型的错误在解决方案方面并不特别有用。这些区域之一是解码。

我已经失去了我帮助弄清楚错误的新开发人员数量的数量,而且我经常看到同样的错误。 他们丢弃了错误,可以帮助他们追踪错误。在本文中,我们将查看一些解码错误。它们是一类错误的一个很好的例子,其中丢弃的错误通常是理解出了问题的最佳工具。它也恰好是一个上下文,很容易显示一些特定的示例,因此它适用于此大小的文章。但是信息到处都是真实的。错误及其携带的消息是有原因的,它们通常是查找错误和使您的代码更强大的有用工具。

所以不要把它们扔掉!

让我们看一个特定示例。为了保护多年来我帮助解码错误的各种人的自我(现在至少有一个在苹果工作),我做了一个示例项目,而不是使用某人的实际代码。但这说明了我在现实世界中看到人们遇到的确切问题。

示例应用程序概述

我的示例应用程序可让您从电视节目“鲍勃的汉堡”中查找随机角色。您可以点击一个按钮,它将加载一个随机字符,向您展示他们的图片,他们的名字,职业以及为角色配音。该角色也有指向狂热Wiki的链接。它使用this 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. 它通过将“字符”和给定的ID添加到API的基本URL
  2. 来构建URL
  3. 它试图从该URL获取数据
  4. 它试图从该数据制作Character
  5. 如果成功,它将返回该字符,否则它将返回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没有返回字符模型。这不是解码问题,而是服务器或我们正在提出的请求的问题。回顾the documentation,我注意到这条路线实际上是/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"。因此,如果我们跳回the documentation并向下滚动到“角色架构”,我们会发现钥匙实际上称为"voicedBy"。同时,我们可能会注意到,我们定义为"wiki_url"的那个实际上是"wikiUrl"。 (如果我们没有碰巧注意到,错误会告诉我们下次我们运行它。)

所以让我们修复并重新运行。在这种情况下,我们实际上可以摆脱CodingKeys枚举,因为我们的所有属性名称都与JSON中的钥匙匹配。

//enum CodingKeys: String, CodingKey {
//    case id, name, image, occupation
//    case voicedBy = "voiced_by"
//    case wikiUrl = "wiki_url"
//}

现在我们可以重新运行并在屏幕上查看一些实际字符!

bobs-characters.png

但是,如果您点击字符足够长的时间,您会发现其中一些无法解码。让我们看看错误说什么:

keyNotFound(CodingKeys(stringValue: "voicedBy", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"voicedBy\", intValue: nil) (\"voicedBy\").", underlyingError: nil))

有趣。这是与以前相同的错误,直到现在我们知道我们的关键名称正确。回到the documentation!在“角色模式”部分中,我们可以看到:

类型 描述
配音 字符串 /未定义< / td> 角色的配音演员

这就是这种API传达voicedBy的方式,如果有的话,将是String,或者如果没有,则将是未定义的(或nil)。在Swift中,实际上是我们称之为可选字符串(String?Optional<String>)的一种不同类型,因此让我们更新模型。当我们使用时,让我们检查一下是否应该是可选的。

我唯一看到的是occupation,所以我们也会更新该occupation。现在我们的模型看起来像这样:

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

现在,我们可以将鲍勃的汉堡角色掌握在我们内心的内容上,而解码永远不会失败。但是,如果确实如此,我们仍然将该错误打印到控制台上,应该能够快速跟踪问题。

嵌套示例

我在文档中注意到某些字符具有relatives数组。如果某个角色具有也在系统中的知名亲戚,它将以纤细的角色模型的数组返回。这似乎是有趣的信息,所以让我们将其添加到我们的应用中。

我将称此版本的Model 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,我们可能很想认为这是我们Character模型上的wikiUrl,因为它有其中之一。但是,如果我们继续阅读,我们可以在DecodingError.Context中看到它正在查看Key relatives(我们知道是一个数组),在0索引项目(意思是数组中的第一个项目),并且不能在那里找到一个称为wikiUrl的键。这意味着我们数组中的第一个亲戚没有wikiUrl

这很有趣,因为the docswikiUrl是不可行的,但显然是。我打印了这个角色的JSON,这是relatives返回的内容:

"relatives" : [
    {
        "name" : "Unnamed child"
    }
]

显然,至少有一个没有wikiUrlurlRelative,我们可以通过制作这些可选的(或者只是不解码它们)来解释模型中的,因为我们没有使用它们)。

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

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

这是一个很好的提醒,文档是一个很好的起点,但并不总是最新的。如果您可以访问维护后端的开发人员,则最好提醒他们差异,以便他们可以将后端逻辑提高到SPEC,或更新文档以匹配实际逻辑。或者,如果是开源API,则可以打开问题和/或做出贡献!

您可以从这个稍微复杂的示例中看到,即使问题深深地嵌套在JSON中,这些错误也有助于解决根部问题。它们可能不是最可读的形式,但是可以努力努力并获得宝贵的见解。而且,与大多数事情一样,您将在练习方面变得更好,并最终能够浏览它们以获取所需的一切。

包起来

这个简单的应用程序中的示例似乎很容易,但是它们反映了当您尝试使新的网络界面启动和运行新的网络界面时所进行的调试。我已经看到人们花了很多时间试图解决他们的解码问题,并提出了各种奇怪而复杂的方法来尝试查明问题。但是,正如我们今天所看的那样,错误,虽然它们似乎很吓人并且一开始很难阅读,但通常充满了有用的信息,这些信息确实告诉我们问题是什么。

所以下次您想把这个错误丢弃时,don't!

让我知道您对错误以及何时/何时不使用它们的意见。并在this branchthis branch上找到完整的入门项目。

发布脚本

并非所有错误在调试代码中都像在解码的背景下一样有用。就像螺丝驱动器对驾驶螺钉非常有帮助,但对于驾驶指甲也不是如此,错误也有助于追踪和解决一些问题,而对其他问题也更少。我的目的只是指出它们是一个有用的工具,并且经常被新开发人员忽略。作为开发人员成长的一部分是学习可用的工具,随着时间的流逝,您将在不同情况下会有所帮助。

我们在本文中没有探索的另一件事是何时/如何与用户通信发生了一些错误。这将取决于您的内容/上下文,并且有一点取决于个人品味。但是总的来说,至少将链条置于您的业务逻辑寿命的层次上是一个好习惯。这样,您至少在那里拥有信息,并可以以对您的应用程序有意义的方式做出有关如何处理它的决定。