Sponsor Link
目次
Result とは何か?
Result は、実行結果について、結果の状態(成功/失敗)と 結果(計算結果)をまとめて扱うことができる enum です。
Swift の enum は、その中に値を保つことができるので、結果状態だけではなく、結果の値を保持することができます。
Result<Success,Error>とは何か
Result<Success, Error> と書くと、正常実行された際の結果 Success と エラーが発生した時の Error を保持するような Result という意味になります。
結果を保持するので、実行結果の状態に応じて、計算結果、エラー情報を取得することができます。
具体的には、Result<Int, Error>という方の Result に対して、以下のような switch 文を使って、結果をチェックすることができます。
1 2 3 4 5 6 7 8 9 10 |
testFunc(value: 1) { result in switch result { case .success(let value): // value は、Int 型 print(value) case .failure(let error): // error は、Error 型 print(error) } } |
Result を返す側では、以下のような書き方になります。
1 2 3 4 5 6 7 8 |
let result: Result<Int, MyCustomStringConvertibleError> if value == 1 { result = Result.success(value) // .success を返すときには、結果の値をセットする(この場合は、Int) } else { result = Result.failure(.MoreThanOne) // .failure を返すときには、Error の値をセットする(この場合は、MyCustomStringConvertibleError) } |
Result を使う利点とは?
実行結果状態だけではなく、結果も合わせて保持できることが1つ目です。
これまでの非同期処理では、引数に正常実行結果と合わせて、エラー情報を渡されて、その2つをチェックすることが多かったと思いますが、この情報を合わせた1つの Result が渡されることで、不整合を防ぎやすくなります。
また、Result の実装に、enum が使用されていることで、タイプチェックしやすくなるとともに、switch 等で判定するときにモレヌケが防止できます。
Playground で使ってみる
Playground 上で以下のコードを実行すると、感じがつかめる気がします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import Foundation import Combine enum MyCustomStringConvertibleError: String, Error, CustomStringConvertible { case NoError = "No Error" case NotOne = "invalid Value(not 1)" var description: String { return rawValue } } func testFunc(value: Int, completion: (Result<Int, MyCustomStringConvertibleError>) -> Void) { if value == 1 { completion(.success(value)) // 計算成功として、.success と合わせて値を返す } else { completion(.failure(.NotOne)) // エラー発生で、.failure と合わせて、エラー詳細を返す } } testFunc(value: 1) { result in switch result { case .success(let value): print(value) case .failure(let error): print(error) } } // -> 1 が表示される testFunc(value: 2) { result in switch result { case .success(let value): print(value) case .failure(let error): print(error) } } // -> invalid Value(not1) が表示される |
Result 応用編:非同期処理 の UI に使うケース
UI として、処理がうまくいったとき と うまくいかなかった時で 表示を切り替えることはよくあります。
Result で結果を保持することで、Result の .success, .failure の状態を表示の切り替えに使うことができます。
さらには、非同期の処理が使われていると、処理が正常終了した、処理が異常終了した 以外に、計算途中という状態も持ち得ます。
Result を ?(optional) という形で保持することで、計算途中という状態を持たせることも可能となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
// // ContentView.swift // Result // // Created by Tomoaki Yagishita on 2020/10/17. // import SwiftUI // (1) enum MyCustomStringConvertibleError: String, Error, CustomStringConvertible { case NoError = "No Error" case MoreThanOne = "more than One" case LessThanOne = "less than One" var description: String { return rawValue } } // (2) struct ContentView: View { @State private var intValue = 5 var body: some View { NavigationView{ VStack { Stepper("value: \(intValue)", value: $intValue) // (3) NavigationLink("calc (but it will finish after a while) !", destination: ChildView(intValue: intValue)) } } } } //(4) struct ChildView: View { var intValue: Int // (5) @State private var calcResult: Result<Int, MyCustomStringConvertibleError>? var body: some View { // (6) switch calcResult { case .success(let int): // (7) Text("Value is \(int)") case .failure(let error): // (8) Text("Error happened: \(error.description)") case nil: // (9) ProgressView() .onAppear(perform: { testFunc(value: intValue) { result in calcResult = result }}) } } func testFunc(value: Int, completion: @escaping (Result<Int, MyCustomStringConvertibleError>) -> Void) { // (10) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { let result: Result<Int, MyCustomStringConvertibleError> // (11) if value == 1 { result = Result.success(value) } else if value > 1 { result = .failure(.MoreThanOne) } else { result = .failure(.LessThanOne) } completion(result) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } |
(1) エラー原因情報を含む MyuCustomStringConvertibleError を定義
(2) 起動時の画面を定義。値設定のスライダーと下位ビューへ遷移するためのボタンを表示
(3) 押下されるとステッパーで指定された値を下位ビューへ渡して遷移する
(4) NavigationLink で遷移する下位ビュー
(5) 非同期を想定した関数の実行結果を保持する Result の定義
(6) 保持している Result の状態によって、表示を切り替える switch 文
(7) Result が .success の状態であれば、結果を Text ビューで表示
(8) Result が .failure の状態であれば、エラーの詳細情報を Text ビューで表示
(9) Result が nil であれば、まだ計算が終了していないので、ProgressView を表示
ChildView へ遷移したときには、Result は nil なので、.onAppear を使用して、ChildView 遷移時に関数実行開始
(10) 実行される関数。非同期を想定しているので、3秒待ってから completion を呼ぶ
(11) 渡された値が 1 であれば、成功として 1 を返し、そうでなければ、”1 より大なのでエラー”、”1 より小なのでエラー”という情報を付与してエラーを返す
まとめ
- Result を使うと、実行状態(成功/失敗)と合わせて実行結果を渡すことができる
- Result を生成するときには、.success と合わせて実行結果値、.failure と合わせて Error をセットする
- 受け取った Result をチェックするときには、.success, .failure をチェックする。抜けもれは、Swift 側でチェックしてくれる
- SwiftUI と組み合わせるときには、Result? としてオプショナルにすると、非同期処理と相性が良い
説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。
Sponsor Link