アプリにおいては様々な処理が実行されますが、それらのプロセスのいずれかにおいては必ず処理の失敗(エラー)が発生します。
アプリを作る際には、どのようなエラーが起こりうるのか、またそれらをどのようにユーザーに伝えるのかを考えながら実装しなくてはいけません。
この記事はそうしたエラーハンドリングの基本について記述します。
エラーハンドリングの種類
処理が失敗し、目的の値が得られなかったことを伝える方法は大きく分けて2種類のみです。
- nilを返す(Optional型を用いる)
- Errorをthrowする
また、全ての値は生成または代入によって生じるので、エラーが生じうる選択肢としては
a. 生成においてエラーが発生した場合
b. 代入においてエラーが発生した場合
の2通りが考えられると思います。
以下では、a,bそれぞれの発生原因において1,2のエラーハンドリングを行う場合のやり方を紹介していきます。
1. nilを返す
a. 生成におけるエラーのハンドリング
例えば、特定の値しか取り得ないenumで、case外のrawValueを指定した時は、コンパイルエラーではなくnil
が返ります。
このように、「型は同じだが想定外の値によって生成した場合」をハンドリングしたい場合には、failable initializerという方法を用いてnilが返るようにします。
enum EnumData: String {
case a
case b
}
class Data {
var enumData: EnumValueData?
init(enumData: EnumValueData? = nil) {
self.enumData = enumData
}
}
class EnumValueData {
let data: EnumData
// failable initializerによって、enum外のrawValueを指定した場合、この`EnumValueData`インスタンス自体がnilになる。
init?(rawValue: String) {
guard let data = EnumData(rawValue: rawValue) else {
return nil
}
self.data = data
}
}
print(EnumValueData(rawValue: "a")) // Optional(EnumValueData)
print(EnumValueData(rawValue: "c")) // nil
b. 代入におけるエラーのハンドリング
Swiftは型付け言語なので、代入においては型が必ず期待通りの値になるため、代入そのものでエラーが生じることはほとんどありません。
しかし、Optional型の値からプロパティを読み取る場合のみ、注意が必要です。
例えば、以下のようなdata
から、text
を読み取りたい場合です。
class Data {
var stringData: StringValueData?
init(stringData: StringValueData? = nil) {
self.stringData = stringData
}
}
class StringValueData {
var text: String?
init(text: String? = nil) {
self.text = text
}
}
let data = Data(stringData: .init(text: "this is data"))
この場合、stringData
が存在しない場合がありますので、data.stringData.text
という書き方をしてしまうと、text
というプロパティがそもそも存在しないのでエラーとなります(実際にはSwiftのコンパイルエラーが出るため、このようなことは起こりません)。
このような際には、Optional Chainingと呼ばれる、nil
になりうる値に?
をつける方法を用いると、text
のような下部プロパティを読み込む前に、その値自体がnil
を返すようになります。
print(data.stringData.text) // stringDataがない場合があるため、コンパイルエラー
print(data.stringData?.text) // this is data
複数の値を同時にハンドリングする場合
SwiftにはcompactMap
と呼ばれるメソッドが存在し、これによりOptional要素からなる配列をnil
を排除した非Optional型配列に変換してくれます。
let dataArray = [Data(enumData: EnumValueData(rawValue: "a")),
Data(enumData: EnumValueData(rawValue: "aaa")),
Data(enumData: EnumValueData(rawValue: "b"))]
print(dataArray.map { $0.enumData }) // [Optional(EnumValueData), nil, Optional(EnumValueData)]
print(dataArray.compactMap { $0.enumData }) // [EnumValueData, EnumValueData] <- 真ん中のnilは排除されている。
2. Errorを返す
Swiftでは例外処理を発生させる方法として、throw
を使うことができます。
それらの例外処理は、throw
可能なprotocolであるError
プロトコルに準拠した構造体を作成することにより定義可能です。
Errorプロトコルの作成方法
送出される例外は複数の選択肢の中の1つのみ、という特性はenum
と非常に相性がいいです。
例えば、以下のような「Stringを正の整数に変換する関数」があったとします。
func getPositiveInt(from string: String) throws -> Int {
if let intValue = Int(string) {
if intValue >= 0 {
return intValue
}
// 負の場合、エラー。
} else {
// Intに変換できない場合、エラー。
}
}
上記の場合、考えうるエラーとしては、「Intに変換できない場合」「Intが負数の場合」の2パターンです。
それを下記のようにErrorプロトコルに準拠させた形で、全case洗い出します。
enum GetPositiveIntError: Error {
case negativeIntError
case notNumberError
var message: String {
switch self {
case .negativeIntError:
return "negative value detected"
case .notNumberError:
return "cannot convert to number"
}
}
}
そしてgetPositiveInt
関数で上記throwする形に書き換えます。
func getPositiveInt(from string: String) throws -> Int {
if let intValue = Int(string) {
if intValue >= 0 {
return intValue
}
throw GetPositiveIntError.negativeIntError // 追加
} else {
throw GetPositiveIntError.notNumberError // 追加
}
}
定義したエラーは、以下のようにtry
を付与した関数をdo-catch
で囲むことによりハンドリングできます。
var value: Int?
do {
value = try getPositiveInt(from: "-1")
} catch let error as GetPositiveIntError {
print(error.message) // negative value detected
} catch {
print("unknown error occurred")
}
print(value)
a. 生成におけるエラーのハンドリング
computed propertyにおいて例外処理は以下のように記述します。
var currentValue: String = "-1"
var positiveIntValue: Int {
get throws {
return try getPositiveInt(from: currentValue)
}
}
あとはdo-catch
で囲むと、関数の返り値を代入する場合とほとんど同じように書くことができます。
do {
value = try positiveIntValue
} catch let error as GetPositiveIntError {
print(error.message)
} catch {
print("unknown error occurred")
}
b. 代入におけるエラーのハンドリング
こちらで説明している方法です。
複数の処理におけるエラーを同時にハンドリングする場合
以下のような関数を複数回呼ぶ場合を考えます。
func getStringRandomly() throws -> String {
if Bool.random() {
return "someValue"
} else {
throw GetStringError.fail
}
}
enum GetStringError: Error {
case fail
}
これを複数回呼び出してエラーをハンドリングするには二通りの方法があります。
一つ目は全てのエラーを内部で処理する方法、二つ目はエラーをrethrow
する方法です。
エラーを内部で処理する方法
func executeGetStrings(for times: Int,
execution: () throws -> Void) {
for _ in 0..<times {
do {
try execution()
} catch {
print("failed to get string")
}
}
}
executeGetStrings(for: 7) {
print(try getStringRandomly())
} // someValue or failed to get string x7
こちらのように、エラーをキャッチした時の処理を指定してあげることで複数回の呼び出しに対して同時にエラーの対処ができます。
関数内部で起こったエラーは関数自体に影響を及ぼさないので、指定した回数必ず実行されます。
エラーをrethrowする方法
func executeGetStringsThrows(for times: Int,
execution: () throws -> Void) rethrows {
for _ in 0..<times {
try execution()
}
}
do {
try executeGetStringsThrows(for: 7) {
print(try getStringRandomly())
}
} catch {
print(error)
} // someValue x0 ~ x7 -> fail
上記のように、rethrows
という文法を使ってあげると、関数内部のエラーをそのまま関数のエラーとして送出してくれます。
do-catch
で囲んであげた場合には、一度でも失敗したらcatchに行くので指定した回数に満たないままエラーが送出されます。
番外編: クラッシュを引き起こす
Swiftで例外を処理する方法として、プログラムをストップさせると言った方法も存在します。
それがfatalError()
を使う、またはOptional
型のforce-unwrappingである!
を使う方法です。
// fatalErrorを使う
do {
try executeGetStringsThrows(for: 7) {
print(try getStringRandomly())
}
} catch {
fatalError("fail!!")
} // Fatal error: fail!!
// force-unwrapping(!)を使う
try! executeGetStringsThrows(for: 7) {
print(try getStringRandomly())
} // Fatal error: 'try!' expression unexpectedly raised an error
しかし、これらは(特にプロダクションで)極力使うべきではありません。
可能な限り、上記二つのエラーハンドリングのうちどちらかを使い、プログラムが停止するのを避けるべきです。
まとめ
- Swiftにおけるエラーハンドリングは2種類:
- nilを返す方法
-
Error
プロトコルに準拠したstructをthrowする方法
- nilを返す方法でのエラーハンドリング
- 生成時のエラーを処理したい場合はfailable initializerを使う
- 代入時のエラーを処理したい場合はoptional chainingを使う
- 複数同時に処理したい場合は
compactMap
を使う
- Errorをthrowする方法でのエラーハンドリング
- まずは起こりうるエラーを
enum
のcase
で洗いだす - 生成時のエラーを処理したい場合はgetterに
throws
を付与する - 代入時のエラーを処理したい場合は関数自体に
throws
を付与する - どちらの場合も、throwしたエラーは
do-catch
で囲んで処理できる - 複数同時に処理したい場合は、内部を
do-catch
で囲むか、又はrethrows
を使って内部のエラーをそのまま関数のエラーとして送出させる- 内部を
do-catch
で囲む場合、指定した回数必ず呼ばれるが、rethrows
を使用した場合、指定した回数呼ばれない場合がある
- 内部を
- まずは起こりうるエラーを
-
fatalError
もエラーをハンドリングする一つの方法だが、使用はできる限り控えなければならない
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。