在 Swift 中,throws 关键字用于表示一个方法或函数可能会抛出错误。在声明方法时,使用 throws 可以表示该方法有可能遇到异常情况,并且会将异常抛给调用者进行处理。调用带有 throws 的方法时,必须使用 try 关键字。
之前学习了如何使用try的三种方法和do-catch来捕获报错问题,为什么还需要使用throws来捕获错误呢?
throws的设计理念
1、定义函数契约
当我们声明一个函数使用throws时,实际上告诉调用者,这个函数可能会失败并抛出错误,你必须为这种情况做好准备。
如果函数没有throws,表示它永远不会抛出错误,调用者不需要考虑错误的情况。这是明确的契约,确保开发者不会忽略错误处理。
2、编译时强制检查
使用throws的函数强制调用者必须使用try,编译器会检查是否处理了可能的错误,这种强制性检查能提高代码的健壮性,因为调用者不能忽略报错。
如果不使用throws,错误可能被直接忽略,可能会导致程序在运行时出现意外的行为。
3、错误传播
如果一个函数遇到错误,而你希望这个错误能被更高层的调用者来处理(例如在链式调用中),throws 允许你将错误 向上传递。
这意味着,你可以编写一个 throws 函数,捕获异常情况并将其抛出给调用者,而不需要立即处理。
4、代码可读性和维护性
通过 throws 明确表明哪些函数可能会失败,可以让代码的可读性更好。其他开发者在使用你的代码时,可以清晰地知道哪些操作需要额外的错误处理。
这使得代码更容易维护,因为所有可能出错的地方都是显式声明的。
因此,throws表示函数可能会抛出错误,并强调调用者处理这些错误。而try则用于在调用throws函数时,显式表示函数的调用可能会失败。do-catch则用于捕获和处理throws函数抛出的错误。
样例解析
func fetchData(from url: String) throws -> Data {
guard url == "https://valid.url" else {
throw URLError(.badURL)
}
return Data()
}
do {
let data = try fetchData(from: "https://invalid.url")
print("Data fetched successfully")
} catch {
print("Failed to fetch data: \(error)")
}
在该代码样例中,我们将fetchData使用throws进行声明,表示可能会抛出错误。
接着,我们使用try来调用fetchData方法,使用try调用throws方法,表示调用者必须明确fetchData可能存在错误。
当fetchData的url赋值失败时,会抛出URLError(.badURL)错误。并由外面的do-catch语句捕获。
throws函数
在之前学习try和do-catch时,通常遇到的是下面的情况:
func decode(tmp: User) {
do {
let encodedData = try JSONEncoder().encode(tmp)
print(encodedData)
} catch {
print("Failed to encode user")
}
}
在这个案例中,我们使用try和do-catch解码数据。虽然没有直接定义一个throws函数,但是JSONEncoder().encoder(_:)本身是一个throws函数。
open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable // JSONDecoder().decoder()为throws函数
这里就是Swift的一个理念:在Swift中,一些方法被设计为“throwing functions(抛出函数)”。这意味着,它们在遇到问题时可以抛出错误,而不是返回正常的结果或者nil。对应这种设计,Swift明确表示该方法可能会抛出错误,并提供错误处理机制,因此,我们必须使用try来捕获可能抛出的错误。
所以,虽然之前在使用try和do-catch方法捕获时,没有遇到throws函数声明,但实际上使用try调取的方法,都是通过throws来抛出错误的。
throws的错误抛出
在 throws 函数中,错误的抛出既可以使用系统提供的错误类型,也可以自定义错误类型。关键在于你 如何定义错误,然后通过 throw 关键字来抛出这个错误。
1、系统的错误类型
Swift 的标准库中已经有很多常见的错误类型,比如 URLError、DecodingError 等等。你可以直接使用这些错误类型来抛出系统定义的错误。例如:
func fetchData(from url: String) throws -> String {
guard url == "https://valid.url" else {
// 使用系统提供的 URLError 类型
throw URLError(.badURL)
}
return "Data fetched"
}
do {
let data = try fetchData(from: "https://invalid.url")
print(data)
} catch {
print("Error: \(error)")
}
在这个例子中,URLError(.badURL) 是系统提供的错误类型。通过 throw 关键字,fetchData 函数抛出了这个错误。
2、自定义错误类型
有时候,系统提供的错误类型不能完全满足你的需求。这种情况下,你可以 自定义错误类型。自定义的错误类型需要遵循 Error 协议。
自定义错误的步骤
1、定义一个遵循 Error 协议的枚举(推荐使用枚举来表示错误,因为它可以涵盖多种错误情况)。
2、在 throws 函数中,通过 throw 抛出自定义错误。
// 定义自定义错误类型
enum NetworkError: Error {
case invalidURL
case noConnection
case timeout
}
// 抛出自定义错误
func loadData(from url: String) throws -> String {
guard url == "https://valid.url" else {
throw NetworkError.invalidURL
}
// 假设还有其他情况可能导致错误
throw NetworkError.timeout
}
// 捕获并处理自定义错误
do {
let result = try loadData(from: "https://invalid.url")
print(result)
} catch NetworkError.invalidURL {
print("Invalid URL provided.")
} catch NetworkError.timeout {
print("Request timed out.")
} catch {
print("An unexpected error occurred: \(error)")
}
在这个例子中:
NetworkError 是一个自定义的枚举,它表示几种不同的网络错误。
loadData(from:) 函数根据具体情况抛出不同的 NetworkError。
在 do-catch 中,你可以根据具体的错误类型做不同的处理。
throws错误抛出总结
使用系统错误:直接利用 Swift 内置的错误类型,比如 URLError、DecodingError 等,通过 throw 抛出。
自定义错误:可以通过定义遵循 Error 协议的枚举(或类、结构体)来自定义错误类型,然后在 throws 函数中使用 throw 抛出这些自定义错误。
自定义错误类型的好处是可以更清晰地表达你的程序中特定的错误情况,使代码更加易读和可维护。
throws的错误传递
在throws的设计理念中有提到链式调用,throws运行将错误向上传递。
这两句话的意思是,使用throws函数可以让错误从当前函数抛出,并传递到调用这个函数的地方处理,而不必在当前函数中直接处理。
链式调用
链式调用是指多个函数调用之间相互依赖,每个函数可能会调用其他函数,例如:
func fetchData() throws -> String {
// 从网络获取数据
return try loadFromNetwork()
}
func loadFromNetwork() throws -> String {
// 模拟网络加载失败
throw URLError(.badServerResponse)
}
在上面的例子中:
- fetchData()函数调用了loadFromNetwork()。
- loadFromNetwork()函数会抛出错误。
这两个函数的调用就是链式调用,因为fetchData()依赖于loadFromNetwork(),并且如果loadFromNetwork()发送错误,整个链条的操作都会失败。
错误向上传递
错误向上传递是指一个 throws 函数中的错误会沿着调用链,一直抛到调用者那里,而不需要在当前函数中立即处理。也就是说,如果一个函数抛出了错误,并且没有在函数内部处理,那么错误会传递到更高层调用这个函数的地方,让调用者来处理。
还是继续上面的例子:
do {
let data = try fetchData()
print(data)
} catch {
print("Failed to fetch data: \(error)")
}
fetchData() 本身并没有处理 loadFromNetwork() 抛出的错误,而是 将错误向上传递。
do-catch 中的 try fetchData() 会捕获 loadFromNetwork() 抛出的错误,然后在 catch 块中处理。
通过 throws,你可以设计出这样的函数结构,使得每个函数不必自己处理错误,而是可以选择将错误向上传递,最终在适当的地方统一处理。
为什么不在fetchData()中直接处理错误?
继续上面的例子,正常来讲fetchData()因为调用throws函数,应该在fetchData()内部处理报错。
return try loadFromNetwork()
值得注意的是fetchData()的调用方式是return。Return的作用是调用loadFromNetwork()并返回其结果,所以会将错误传递给调用fetchData()的调用者。
func fetchData() throws -> String {
// 从网络获取数据
return try loadFromNetwork()
}
func loadFromNetwork() throws -> String {
// 模拟网络加载失败
throw URLError(.badServerResponse)
}
如果loadFromNetwork()没有抛出错误,它的返回值也会通过return返回给调用fetchData()的代码。
如果loadFromNetwork()抛出错误,try会捕获这个错误,并将错误向上传递,让fetchData本身也抛出错误。
同理,当我们的loadFromNetwork()也调用throws函数时,也是通过return来返回结果,这样变成了依次调用,return依次返回,这也是我们的错误向上传递的理念。
为什么要这样设计?
假如每个函数都要自己处理错误,那会导致以下问题:
1、代码重复:多个函数中会出现相同的错误处理逻辑,使得代码冗长、难以维护。
2、不灵活:某些情况下,你希望错误被最顶层的调用者处理,而不是在每一层都进行处理。throws 允许你直接将错误抛出到调用者那里,而不做额外的处理。
throws错误传递总结
链式调用:一个函数依赖于另一个函数的调用,这种连续的调用关系称为链式调用。
错误向上传递:throws 允许一个函数在遇到错误时,不在当前函数处理,而是将错误抛出到调用者,直到在更高层的 do-catch 中捕获处理。这减少了冗余的错误处理代码,使程序更简洁和灵活。
总结
学习throws之后,可以更好的理解并使用throws、try和do-catch等方法,使用throws的函数都需要使用try进行调用,以便调用方能够明确这是可能抛出错误的方法。
在理解throws的错误传递后,我们可以知道throws只会在调用该throws函数的地方报错,因为throws会通过return逐级返回。
如果仍然不太理解这个throws的调用,我可以分享一个简单的故事,那就是一个市长安排秘书处理一个影响城市市容的污水,秘书可能会通知污水沟所在的乡镇处理,然后乡镇方面安排具体的人员来处理这个问题。
当人员无法在规定的时间内处理,那么它们就需要向乡镇汇报、乡镇反馈秘书、秘书告诉市长,这就是链式调用。
当市里因为污水被通报时,看似问题是因为乡镇安排的人员没有处理完成,实际是最后会因为污水而通报到市里,影响到市长。所以这就是错误的向上传递,因为错误在于没有按时完成,错误最终是影响到市长本人。
而throws在这个故事中充当的就是逐级安排和逐级上报,throws抛出的问题就是逐级上报上来的。
相关资料
1、Xcode报错:Call can throw, but it is not marked with ‘try’ and the error is not handled:https://fangjunyu.com/2024/09/25/xcode%e6%8a%a5%e9%94%99%ef%bc%9acall-can-throw-but-it-is-not-marked-with-try-and-the-error-is-not-handled/