Swift: Как использовать stderr с типом ошибки? - PullRequest
1 голос
/ 28 апреля 2020

Мне интересно, есть ли простой способ использовать ошибку Swift и одновременно записывать ее в stderr. Например, в моем приложении CLI у меня есть это перечисление для ошибок:

enum ErrorList: Error {
    case alreadyInList
}

extension ErrorList: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .alreadyInList:
            return NSLocalizedString(
                "This item is already in the list.",
                comment: "Info message")
        }
    }
}

Ранее я использовал перечисление basi c OutputType для записи в stderr:

fputs("\u{001B}[0;31m\(message)\n", stderr)

Есть ли способ легко объединить оба? Если я использую указанную выше строку печати с перечислением ErrorList, я получаю эту ошибку:

Невозможно преобразовать возвращаемое выражение типа 'Int32' в возвращаемый тип 'String?'

Как поступить? Какой подход является лучшим?

Редактировать: Ниже я попробовал подход @ Sweeper следующим образом:

func validate() throws {
    guard bla bla else {
        let error = ErrorList.alreadyInList
        fputs("\u{001B}[0;31m\(error.errorDescription)\n", stderr)
        throw error
    }
}

func run() throws {
    try validate()
    bla bla
}

Приведенный выше код дает мне двойной, но правильный вывод (красным):

Этот пункт уже в списке.

Ошибка: Этот элемент уже есть в списке.

Для приведенного ниже кода я получаю только

Ошибка: этот пункт уже есть в списке.

func validate() throws {
    guard bla bla else {
        throw = ErrorList.alreadyInList
    }
}

func run() throws {
    do {
        try validate()
        bla bla
    } catch let error as ErrorList {
        fputs("\u{001B}[0;31m\(error.errorDescription)\n", stderr)
    }
}

Ответы [ 2 ]

1 голос
/ 28 апреля 2020

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

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

Итак, вход в свою собственную функцию, которая принимает ошибку и возвращает ошибку.

func logging(_ error: LocalizedError) -> LocalizedError {
    fputs("\u{001B}[0;31m\(error.errorDescription ?? "")\n", stderr)
    return error
}

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

func validate() throws {
    guard ... else {
        throw logging(ErrorList.alreadyInList)
    }
}

Но что, если вы хотите изменить способ ведения журнала? А как насчет юнит-тестирования и все такое? Не проблема. В Swift функции первого класса, так что вы можете передавать их, и они могут даже иметь значения по умолчанию.

typealias Logger = (LocalizedError) -> LocalizedError

struct Operation {
    let logging: Logger
    init(logging: @escaping Logger = standardLogging) {
        self.logging = logging
    }

    func validate() throws {
        guard false else {
            throw logging(ErrorList.alreadyInList)
        }
    }
    // ...
}

И теперь вы можете создать Operation с помощью стандартного регистратора тривиально, но для модульного тестирования Вы можете сделать это:

class LogAccumulator {
    var logs: [Error] = []

    func logging(_ error: LocalizedError) -> LocalizedError {
        logs.append(error)
        return error
    }
}

let logs = LogAccumulator()
try? Operation(logging: logs.logging).validate()
print(logs.logs)

У меня был большой успех модульного тестирования путем проверки выходных данных журнала. (Я считаю, что вывод журнала является явной частью интерфейса, который должен быть протестирован вместе с другим поведением.) Он может позволить вам протестировать все виды вещей, которые сложно проверить, не публикуя личные данные c.

1 голос
/ 28 апреля 2020

, но я не могу использовать его как throw fputs("\u{001B}[0;31m\(ErrorList.alreadyInList)\n", stderr)

Вам просто нужно разделить его на два оператора:

let error = ErrorList.alreadyInList
fputs("\u{001B}[0;31m\(error.errorDescription)\u{001B}[0m\n", stderr)
throw error

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

throw ErrorList.alreadyInList

, а в вызывающем методе throws вы будете использовать оператор do...catch, например, так:

do {
    try throwsMethod()
} catch let error as ErrorList {
    fputs("\u{001B}[0;31m\(error.errorDescription)\n", stderr)
} catch {
    // handle errors that are not ErrorList
}

Видя ваши изменения, вот что я имел в виду:

Первый подход:

func validate() throws {
    guard bla bla else {
        let error = ErrorList.alreadyInList
        fputs("\u{001B}[0;31m\(error.errorDescription)\n", stderr)
        throw error
    }
}

func run() throws {
    do { 
        try validate()
    } catch {
        // handle the error some other way
        // don't log it again! You already did it!
    }
}

Второй подход:

func validate() throws {
    guard bla bla else {
        throw ErrorList.alreadyInList
    }
}

func run() throws {
    do {
        try validate()
    } catch let error as ErrorList {
        fputs("\u{001B}[0;31m\(error.errorDescription)\n", stderr)
    } catch {
        // handle other errors...
    }
}
...