https://youtu.be/ytp74XaPMEU
今回も引き続き The Basics の オプショナル
について読み進めていきます。これまでは概念的な観点での基本や Objective-C 言語との違いについてを見てきたので、今回はその具体的なところの基礎を確認していきます。日常的に Swift を使っている人は オプショナル
に既に親しむ機会も多いと思いますけれど、Swift を支える根幹にある機能のひとつで言語による支援が多い機能なのと、親しむほどに使い方に個々の癖が出てくることもあったりすると思うので、少し初心に戻る気持ちで オプショナル の初歩に触れる機会にしてみると良いことあるかもしれないです。よろしくお願いしますね。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #138
00:00 開始 00:08 今回は北海道、札幌からの開催 00:56 オリエンテーション 01:22 勉強会の目的 02:22 勉強会の方向性 02:52 お気軽に話しかけてもらうための心持ち 03:39 オプショナルについての前回のおさらい 04:26 無効な値にオプショナルで対処する例 05:16 失敗可能イニシャライザー 06:40 先ほどの例を Playground で実際に見てみる 07:27 失敗可能イニシャライザーはオプショナルで返す 08:06 エラー処理の匙加減 09:51 エラーハンドリングをオプショナルに変換 10:30 標準でエラーハンドリングを使うイニシャライザーはある? 12:21 エラーハンドリングを用いる場合の判断基準 13:56 イニシャライザーが受け取ったクロージャーがエラーを想定しているとき 14:29 イニシャライザーとエラーハンドリングの組み合わせは少ない様子 15:14 Swift におけるエラーの分類 17:35 オプショナルとエラーハンドリングの併用 18:28 イニシャライザーで初期化は失敗してもエラーではない? 19:53 関数が nil を返しても失敗とは限らない 22:34 文字列を整数に変換できないときはエラー扱い? 24:07 エラーとリカバリー性 24:44 fatalError では defer は発動しない 26:32 テストによって不備が摘み取られる論理エラー 27:14 precondition と未定義動作 30:28 設計者の意図により採用するエラーの種類は異なる 32:30 添字アクセスを論理エラーとするか単一ドメインエラーとするか 33:22 配列の有効なインデックス範囲 34:50 なぜ、論理エラーとして分類しているのかという観点 36:09 無チェック最適化をかけたときの添字アクセス 38:11 preconditionFailure が Never を返す? 39:49 クロージングと次回の展望 40:29 Never の気になるロスタイム ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #138
こんにちは、よろしくお願いします。今日は、津川さんがいらしています。あともう一方、地元のコミュニティを開催している方が来る予定ですが、少し遅くなるようです。今、オフライン会場となっていますが、ほぼオンラインです。現在ここにいるのは一人で、もう一人は遅れてくる予定です。どなたか札幌にいる方はいないでしょうか?今からでも遅くありませんので、遅れても構いません。
さて、現地に来てくださった方もいますので、そろそろ始めていきましょう。今日は初めて来た方もいらっしゃるので、軽くオリエンテーションっぽいところからお話しします。この勉強会はプログラミング言語の基礎学力を養うことを目的としています。株式会社ゆめみの中でのメインターゲットはアソシエイトエンジニアですが、経験のある方も含めて、興味がある方ならどなたでも歓迎です。この勉強会を通じてプログラミング言語の存在に意識を向け、自身で考える力を養うことができれば良いなと思っています。
題材としては『The Swift Programming Language』、Apple公式の本を用いています。時間をかけてゆっくりと広く深く見ていくのがテーマです。同じことを何回も話すことがありますが、一度聞いて身につくわけではないので、何回も聞いて馴染んでいけたら良いかなと思います。
それでは、交話を交えながら進めていきたいと思います。いつでも話しかけてくださって構いません。どんなに詳しそうな人でも些細なことを知らないことはたくさんありますので、みなさんも遠慮なく、どんなことでも話してくださって大丈夫です。
さて、今日はオプショナルについてお話しします。前回と前々回でオプショナルの概念をざっくりと説明しましたが、今日はオプショナルの基本的な具体的特徴について話していきます。前回、Int.max
みたいな特殊な値を使うといろいろと厄介ですが、nil
を使うことによって正確に値がないことを表現できるという話をしました。今日は、具体的な使用例についても見ていきます。
まず、値がないときにオプショナルを使ってどのように対処するかという例です。例えば、文字列を数値に変換する際、全ての文字列が数値に変換できるわけではありません。そのため、変換に失敗したときにはnil
を返すイニシャライザーが用意されています。例として、"123"
という文字列をInt
のイニシャライザーに渡して値を取得する場合がありますが、この例だけではわかりにくいかもしれませんね。この辺りの解説から入りましょう。
成功の例として、次に解説を読んでいく前に、もう少し具体的な話をしていきたいと思います。それでは進めていきましょう。 もう少し先に進むと、nilの話になりますね。しかし、失敗の例などはまだ書いていないので、今回はもう少し先に進む部分と絡めて説明を続けます。間を取り持つためにも、一旦ここでPlaygroundを使って見ていきましょう。このintの変換の部分です。
それでは、先ほどのオプショナルの例を見ていきます。以前の例では1 2 3
という数字を let possibleNumber
にしていましたが、ここではその名前をnumber
に変更しましょう。まずlet number = "123"
というコードを書きます。次に、let convertedNumber = Int(number)
として、そのnumberを渡して実行します。これで普通に123
という感じで出てきますね。これは問題なく動作します。
ただし、このコードは全然オプショナルが活かせている感じがしません。まず大事になってくるのは、ここです。Int
のイニシャライザですね。これが失敗可能イニシャライザになっていることがポイントです。文字列を渡して、その文字列が整数に変換できるときにはイニシャライザが成功し、Int
型の値が返ってきます。失敗するときにはnilが返ってくる、こういったイニシャライザになっています。
最初にこのバランス感覚が面白いなと感じました。理由を知らないとエラースルーしてしまうこともありますが、かなり面倒な感じもします。しかし、このようにいくつかオプショナルを使った例で、失敗した場合にもオプショナルが返る設計がされています。イニシャライザが失敗可能であれば、単に失敗したとだけ分かれば十分な場合が多いです。
エラースルーしたくなる場面もありますよね。例えば、そのまま構造体を使ってイニシャライザが失敗するかもしれない場合、単にdescription
を受け取ってスローすることが考えられます。これは、呼び出し元にどういった理由で変換できなかったかを細かく伝えたいときに適しています。実際にそうしたい場合、この方法は選択肢に上がってきます。
ただし、失敗可能イニシャライザを使うと、理由はどうあれ失敗したことがオプショナルで分かります。このようにオプショナルバインディングを使って、value
のディスクリプションを書いて、オプショナルバインディングを簡単に書けるので、使い勝手が非常に良いです。ただ、これだとスローしてしまっているため、この辺りが使いやすいけど個人的には好きなポイントです。
仮にスローズだったとして詳細なエラーが出た場合も、このトライキャッチを使うと普通のエラーハンドリングができます。これがとても好きなんですよ。
失敗しても手軽に扱いたい場合、失敗可能イニシャライザを選ばなくても、APIを利用する側が選んで使えるのが素晴らしいところです。
具体的なガイドラインがあるとすれば、構造体をスロー可能にすれば良いのではないかとも思いますが、どうでしょうね?標準プレイマークにはスローが使用されているかもしれませんが、確信はありません。そのユニットテストなどでは、おそらくスローを返す標準ライブラリもあり得ますが、なかなか使わないことが多いですね。最近のアプリでは、エラーハンドリングも少ないですよね。
例えばGitHubでユニットや標準ライブラリを探すと、スローが使用されている部分を見つけることができます。この方法で、例えばunit
やregular expressions
を調べて、関連するリポジトリを見つけ出せます。確かに、こういったエラーハンドリングは具体的な理由が分かりやすいですし、APIの使用元にリカバリー手段を提供できます。なので、エラーハンドリングを使うかどうかの判断基準の一つに、そういったリカバリー可能性を考慮することも必要ですね。 、その理由だったときは、これをして待機を図るなどの対応を行います。要はリカバリーのための手段として、何らかの値を返すのがエラーハンドリングです。オプショナルの「?」を使う場合も同様で、失敗したときに対処方法を見つけるために使います。ただし、理由を問わず失敗したときの補足的な処理として、ユーザーに判断を持たせる程度といったところです。
「throws」を使うとより詳細になりますので、アプリケーションの用途によって適しています。たとえば、重大なエラーが発生した場合には、アプリ再起動を促すとか、この程度のエラーなら5秒後にリトライすればよいといった具合に、ソフトウェアが自発的に判断できます。ユーザーに判断させたいときはエラーメッセージを表示するだけでも良いので、そこまで「throws」を使わなくても良いかもしれません。
他には公式のドキュメントに記載されている内容ですと、クロージャー内でスローを使ったり、イテレータからシーケンスを作る際にエラーを返すことが考えられます。リザルトモナドやリザルトタイプのクロージャーについても同様に扱います。
エラーハンドリングに関する公式のガイドラインが必ずしも全てのケースに言及しているわけではありませんが、一般的な指標として、「ドメインエラー」「ディテクタブルエラー」「ユニバーサルエラー」「ロジックフェイラー」などのカテゴリー分けがあります。これらは必ずしも絶対的ではなく、価値観によって異なる場合もありますが、判断材料として活用できます。たとえば、初期化子(イニシャライザー)をエラーハンドリングで失敗可能にするかどうかも、こういった指標があると判断しやすいです。
コメントいただいている内容や公式ドキュメントの具体例を確認しつつ、エラーハンドリングや「throws」の使い方を見ていきます。たとえば、「イニシャライザー」でスローズを使うことがほとんどないケースもあるため、基本的には「フェイラブルイニシャライザー」を使います。
両方を併用することは少なく、ファンクションで何かを行う際にエラーを返すと同時にオプショナルを返すようなことは、あまり見かけません。正常な状態としてのオプショナルを返す場合や値がないことが正常なときに使う程度です。ただし、APIが複雑化し、読みづらくなることもあるため注意が必要です。
正常な処理としてのイニシャライザーもありますが、目的はインスタンスを作成することなので、作れなかったら失敗と考えるのが一般的です。一方で、メソッドは必ずしもインスタンス化のみを目的としているわけではないので、使い分けが求められます。 イニシャライザーが失敗する可能性についての話ですが、確かに「失敗は成功のもと」という言葉もあります。ただ、イニシャライザーに関しては役割が決まっているので、その範囲を外れた場合に「失敗」と感じるかもしれません。これは、あくまで個人的な感覚の話です。
次に、関数が結果を返す場合についてです。結果がnil
ならばそれもOKな場合もあります。例えば、find
関数なんかがそうです。他にもfirstWhere
がありますね。firstIndex
も失敗を捉えることがありますが、これもnil
を使うことがあります。昔のAPIでよくあったのは、findIndex
などです。その場合、検索して見つからなかったらnil
を返す。それは、検索という操作の正常な結果と捉えることもできるかもしれません。
ファイルキャッシュをイニシャライザーで失敗可能にして、その場合にthrows
を使うと、キャッシュがない場合やファイルアクセスに失敗する場合にエラーを投げることができます。しかし、キャッシュに存在しなかっただけでエラーとするのは少し乱暴かもしれません。
このように、エラーハンドリングに関しては考え方がいろいろあります。キャッシュがファイルにないのはエラーではないと納得できる場合もありますし、あるいはエラーと考える場合もあります。また、Optional
を使って、エラーが発生する場合はnil
を返すといったシンプルなエラーハンドリングもあります。
一方で、リカバリーができないエラーについては、例えばファイルIOのエラーの場合、fatalError
でクラッシュするといった処理も考えられます。しかし、その場合でもdefer
を使ってリソースの解放をきちんと行う必要があります。fatalError
の前にdo-catch
で適切にリソースを解放するためのコードを入れておくことが重要です。
最後に、ロジックエラーについてです。これは例えば、Int型の引数に対して不適切な値を渡す場合などが該当します。プレコンディションを使って前提条件を保証しておけば、テストが完璧である限りその条件が除外されることが期待されます。実際の開発において、これらのエラーハンドリングの方法を適切に選択することが求められます。 未定義の動作やプレコンディションという用語をご存知でしょうか。プレコンディションはフェイタルエラーとは少し雰囲気が異なります。基本的には関数を作成し、パラメータを取り、戻り値も返すように設計します。Int
型など何でも良いのですが、フェイタルエラーの場合、処理を中断させることが基本的な考え方にあります。例えば、条件分岐の際にフラグが設定されていた場合にはフェイタルエラーで落とし、そうでなければ結果を返すといったコードを書くことが考えられます。
そのため、特定の条件下でランタイムエラーを発生させる状況がある一方、プレコンディションの場合は、不正な値が来たときにそれ以上の処理を進めないようにして、エラーメッセージを通知します。プレコンディションは、エンジニアがエラーを適切に修正するきっかけになります。しかし、最適化の過程でアンチェックされる可能性がある場合、そのプレコンディションが取り除かれる恐れがあります。この時、パスやロジックが誤ってしまう可能性があるため、ロジック的には一貫しているが、コンパイルエラーとしておかしい状況が発生します。フェタルエラーは、この関数内でのエラーになります。
例えば、数値変換が確実に成功することが前提の場合、フェイタルエラーを使います。具体的には、Int
型の初期化子(イニシャライザー)として渡された値が成功する場合、うまく変換できなかったらフェイタルエラーとして処理を中断します。これは厳格過ぎると感じるかもしれませんが、理にかなったケースです。
また、APIの設計者として、意図に基づいてプレコンディションやフェイタルエラーを適用するかどうかを選択することが重要です。例えば、ディスクリプションエラーはメモリ不足やスタックオーバーフローなど、回避不可能なエラーとして扱われます。一方、配列のインデックスアクセスのエラーなどは場合によっては異なる対応が必要です。
例えば、配列のインデックスアクセスで値が取れなかった場合にnil
を返すという考え方があります。ただし、リカバリが不可能なエラーとして扱う場合も考えられます。これは、設計者の意図により、インデックスが有効かどうかを検査する際に役立ちます。
具体的なコード例を示すと、以下のようになります:
func fetchElement(from array: [Int], at index: Int) -> Int? {
guard array.indices.contains(index) else {
return nil
}
return array[index]
}
このように、配列のインデックスが有効であるかを検査するコードを書くことは、安全性を高めるための良い習慣です。 なかなかこの辺の、どのエラー処理手段を使うかという問題は難しいですね。標準のサブスクリプトを使うと、この前の例ではロジックエラーを選んでいました。インデックスチェックは自動で行われて、必ず正確なインデックスを渡すようになっています。
このような違いは、オーバーヘッドを減らすかどうかという観点でも重要です。個人的には、処理コストをどうするかという点が大きい要素だと思います。例えば、パフォーマンスを考えるなら、インデックス範囲外のアクセスを避けるためにオーバーヘッドを気にするのは重要です。しかし、これは考え方の問題でもあります。サブスクリプトでアクセスする際にエラーが出るのは、コーディングミスと見なしてカバーしないという判断もあります。
確かに、このような考え方の違いがあるので、後々のオーバーヘッドを気にせずアクセスする方法もあります。ビジネスロジックの観点から見ると、この違いで結果が変わることもあるでしょう。プレコンディションチェックを導入することで、万全を期すことができます。
メモリープロテクションが入る現代のパソコンでは、許可されたメモリの範囲外アクセスは、最終的にシステムが止めてくれることもありますが、それでもプログラムとして安全に動作させるにはプレコンディションチェックが有効です。
リリースビルドでは、デフォルトでオーバーチェックは行われないことが一般的です。プレコンディションはデフォルトでエラーを引き起こしますが、オーバーチェックはリリースビルドでは省略されることがあります。これにより、変な動作が発生する可能性がありますが、性能のためにはこのトレードオフが受け入れられることもあります。
デバッグビルドの際にはアサートを使うことが多いですが、リリースビルドになるとアサートは無効化されることが一般です。プレコンディションを使うことで、万が一のエラー時にプログラムを安全に停止させることができます。
時間になったので、今日はこの辺で勉強会を終わりにします。また次回、今回の内容を補足したり、現状のプレコンディションやアサートについての詳細も改めて報告します。今日はお疲れ様でした。ありがとうございました。