Обработка исключений в блоках защиты

#swift #exception

Вопрос:

Я ищу элегантный способ объединения guard блоков с do-try-catch семантикой в Swift и до сих пор не был удовлетворен своими собственными усилиями.

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

Однако для остальной части метода нас волнует только то, есть ли предмет для работы — так что это звучит как идеальный случай для guard :

 func myCoolMethod() {
    guard let todoItem = try? fetchItemFromList() else {
        // No item to act on, nothing to do
        return
    }

    // rest of function acts on todoItem
    ...
}
 

Это действительно нормально — до тех пор, пока защитный блок не начнет отказывать, когда вы ожидаете, что он будет успешным, и вы действительно хотели бы (если ничего другого) зарегистрировать сообщение об ошибке, чтобы попытаться провести расследование. Таким образом, try? необходимо заменить a try , чтобы ошибка была зафиксирована, и теперь нам нужно будет завернуть guard ее в do блок:

 func myCoolMethod() {
    do {
        guard let todoItem = try fetchItemFromList() else {
            // No item to act on, nothing to do
            return
        }

        // rest of function acts on todoItem
        ...
    }
    catch {
        // Now we know there was an error of some sort (can specialize the
        // catch block if we need to be more precise)

        // log error or similar, then...
        return
    }
}
 

В этом есть две проблемы, которые я вижу:

  1. do Блок должен охватывать весь метод (из-за области применения todoItem ). Это означает, что логика счастливого пути имеет отступ (что guard особенно помогает избежать); но, кроме того, это означает, что любые ошибки из этой логики будут пойманы catch блоком, возможно, случайно. Я бы предпочел, чтобы этот do блок был ограничен только областью guard .
  2. Там дублируется блок «что делать, если у нас нет товара». В этом тривиальном примере это просто a return , но мне все еще немного больно иметь два отдельных блока, которые должны делать в целом одно и то же (где один просто добавляет немного дополнительного контекста с доступной ошибкой).

Я хотел бы подойти к этому как можно ближе:

 func myCoolMethod() {
    // `do guard` is made up but would enter the else block either if an exception
    // is thrown, or the pattern match/conditional fails
    do guard let todoItem = try fetchItemFromList() else {
        if let error = error {
            // Optional `error` is only bound if the guard failed due to an exception
            // here we can log it etc.
        }

        // This part is common for both the case where an exception was thrown,
        // and the case where the method successfully returned `nil`, so we can
        // implement the common logic for handling "nothing to act on"
        return
    }

    // rest of function acts on todoItem
    ...
}
 

Каковы мои варианты здесь?

Ответ №1:

Во-первых, ищите ошибки и разбирайтесь с ними. Затем ищите существование и разбирайтесь с этим:

 func myCoolMethod() {
    // Making this `let` to be stricter. `var` would remove need for `= nil` later
    let todoItem: Item?

    // First check for errors
    do {
        todoItem = try fetchItemFromList()
    } catch {
        todoItem = nil  // Need to set this in either case
        // Error handling
    }

    // And then check for values
    guard let todoItem = todoItem else {
        // No item to act on (missing or error), nothing to do
        return
    }

    // rest of function acts on todoItem
    print(todoItem)
}
 

Или разделите проблемы на функции. Когда все усложняется, делайте более целенаправленные функции:

 func myCoolMethod() {
    // And then check for values
    guard let todoItem = fetchItemAndHandleErrors() else {
        // No item to act on (missing or error), nothing to do
        return
    }

    // rest of function acts on todoItem
    print(todoItem)
}

func fetchItemAndHandleErrors() -> Item? {
    // First check for errors
    do {
        return try fetchItemFromList()
    } catch {
        // Error handling
        return nil
    }

}
 

Комментарии:

1. Первый блок кода не компилируется, второе объявление todoItem конфликтует с первым. Вы имели в виду использовать разные имена переменных?

2. @MartinR Странно, это компилируется для меня. todoItem намеренно отбрасывает тень на первую. Какую ошибку вы получили?

3. «Определение конфликтует с предыдущим значением» в guard let todoItem = ... строке.

4. @MartinR это странно. Вот моя суть, которую я использую в приложении командной строки. gist.github.com/rnapier/6f14610ce1897f538feaaf779ab90878 Я знаю, что было исправлено исправление ошибки «по собственному определению», которое только что было добавлено в бета-версию 12.5 3 (которую я использую), но я думал, что оно применимо только к функциям и свойствам.

5. Это бы все объяснило, я сейчас использую 12.4.