我想知道是否有一个简单的方法来使用Swift的Error并同时将其写入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")
}
}
}
以前我用的是一个基本的 OutputType
要写入的枚举 stderr
:
fputs("\u{001B}[0;31m\(message)\n", stderr)
有没有一种方法可以轻松地将两者结合起来?如果我用上面的打印行加上 ErrorList
枚举,我得到这个错误。
无法将Int32类型的返回表达式转换为String类型?
该如何操作?哪种方法是最好的?
编辑:下面我尝试了@扫地僧的方法,是这样的。
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
}
上面的代码给了我双倍但正确的输出(红色)。
这个项目已经在列表中了。
错误:这个项目已经在列表中了。这个项目已经在列表中了。
对于下面的代码,我只得到
错误。This item is already in the list.
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)
}
}
但我不能把它当作
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...
}
}
错误只是一个值 你可以自由地操作它们而不扔掉它们。throws
只是一种花哨的 return
. 它可以用任何你喜欢的方式生成错误。
虽然Sweeper提出了很好的观点,对于一个命令行应用程序来说,这种方法可能是最好的,但我经常发现在错误产生的地方而不是在错误消耗的地方记录更好。我发现这样可以使日志更清晰地显示原始错误发生的位置,从而使日志更有用(我一般会记录文件和行信息)。当然,不是每一种错误都应该被记录下来。但根据我的经验,生成者往往比调用者更清楚。
所以,把日志提升到自己的函数中,接受一个Error并返回一个Error。
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)
我通过验证日志输出来进行单元测试,取得了巨大的成功。我认为日志输出是接口的一个显式部分,应该和其他行为一起测试)。它可以让你在不公开私有细节的情况下测试各种难以检查的东西。