Swiftでdeferの中のエラーハンドリングを試みる
Last Updated on 2024年2月15日 by lemonade
deferはSwiftでコードブロックを抜ける際に必ず行う処理として定義できるものです。ファイルのcloseなどクリーンアップ処理に使われることがほとんどです。
しかし、クリーンアップ処理最中に投げられたエラーを伝播させたい時はありませんか?
この記事ではSwiftのdeferでthrowされた場合の方法を模索してみます。
定義
まず、エラーを起こせる対象のファイルクラスを作成します。
struct File {
var onOpen = false
var onClose = false
var onWrite = false
var onRead = false
func open() throws {
if onOpen { throw Error.open }
}
func close() throws {
if onClose { throw Error.close }
}
func write(text: String) throws {
if onWrite { throw Error.write }
}
func read() throws -> String {
if onRead { throw Error.read }
return "Hello, world!"
}
enum Error: Swift.Error {
case open, close, write, read
}
}
そしてこれを以下のように使ってみます。
func 処理(file: File) throws -> String {
// ファイルを開く
try file.open()
// スコープから出る際にファイルを閉じる
defer { try file.close() } // Call can throw, but errors cannot be thrown out of a defer body
// ファイルを読み込む
let text = try file.read()
// ファイルに書き込む
try file.write(text: text)
// ファイルの内容を返す
return text
}
もちろんdeferの中ではthrowをそのままにできないのでエラーが出て実行できません。
方法1 try!で揉み消す
func 処理(file: File) throws -> String {
try file.open()
defer { try! file.close() }
let text = try file.read()
try file.write(text: text)
return text
}
print(try 処理(file: File())) // Hello, world!
try!だと強制的にthrowされないと示しているためthrowされたことにならずコンパイルが通ります。
print(try 処理(file: File(onClose: true)))
// Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: File.Error.close
しかし、もしcloseでエラーが投げられた場合fatalErrorが発生しプログラムが落ちてしまいます。これはcatch後にfatalErrorを投げる場合でも同じです
方法2 キャッチしてログだけ出す。
func 処理(file: File) throws -> String {
try file.open()
defer {
do {
try file.close()
} catch {
print(error)
}
}
let text = try file.read()
try file.write(text: text)
return text
}
print(try 処理(file: File(onClose: true)))
// close
// Hello, world!
これだと、closeされた際にエラーが出たことは気づけますが、そのまま後の処理が続きます。クリーンアップ処理が完了しなくても一旦、後の処理に問題がないのであればこれでいいでしょう。
方法3 inoutでResult型でエラーを返す
defer内では引数は扱うことができるので、inoutでResult型を受け取ることでdefer内で起こったErrorを外に伝えることができます。
func 処理(file: File, result: inout Result<String, any Error>) {
do {
try file.open()
defer {
do {
try file.close()
} catch {
result = .failure(error)
}
}
let text = try file.read()
try file.write(text: text)
result = .success(text)
} catch {
result = .failure(error)
}
}
var result: Result<String, any Error> = .success("")
処理(file: File(onClose: true), result: &result)
print(result) // failure(File.Error.close)
しかし、Errorが2つ起こった場合はどうなるのでしょうか
var result: Result<String, any Error> = .success("")
処理(file: File(onClose: true, onWrite: true), result: &result)
print(result) // failure(File.Error.write)
writeとcloseでエラーが発生した場合、write→close→Error.closeのcatch→Error.writeのcatchという順で起こるため、close時のエラーはwrite時のエラーに上書きされてしまいました。一律でエラーが出たときのハンドリング方法があるのならこれでいいですが、細かく分ける必要がある場合はこれではいけないかもしれません。
方法4 inoutでResultの配列を受け取る
[Error]型はErrorに準拠していないのでextensionします。
extension [any Error]: Error {}
func 処理(file: File, result: inout Result<String, [any Error]>) {
do {
try file.open()
defer {
do {
try file.close()
} catch {
switch result {
case .success(_):
result = .failure([error])
case .failure(let errors):
result = .failure(errors + [error])
}
}
}
let text = try file.read()
try file.write(text: text)
result = result.map { _ in text }
} catch {
switch result {
case .success(_):
result = .failure([error])
case .failure(let errors):
result = .failure(errors + [error])
}
}
}
var result: Result<String, [any Error]> = .success("")
処理(file: File(onClose: true, onWrite: true), result: &result)
print(result) // failure([File.Error.close, File.Error.write])
かなり冗長になってしまいました。
まとめ
とりあえずResultをinoutで扱うことで外にdefer中のエラーは伝えることができました。ただ、冗長になってしまうのでResult.get()でthrowに直すラップ関数を作ったりdoでインラインでget()までしたりしてもいいかもしれません。