Swiftでのエラーハンドリングとエラー耐性についての教訓 | try! Swift Tokyo 2017 #tryswiftconf Day2-15 聞き起こし

twitter.com

ソフトウェアを書いているとき、私たちはハッピーパス(例外やエラーが発生しない正常系のこと)についてはちゃんと考慮する一方、潜在的な障害についての考慮はおろそかになりがちです。しかしアプリが考えていたよりも長く、いろいろな状況で使われるようになると、コードはより複雑に分岐します。この講演では、あなたのアプリのエラー耐性を高めて少しでも'アンハッピーパス'がユーザーやアプリを保守する人たちにもたらす憂鬱を軽減するために、Lineで遭遇した様々なタイプのエラーに対処するために学んだ教訓を紹介します。

Swiftでのエラーハンドリングとエラー耐性についての教訓

f:id:niwatako:20170303175302j:plain

Swiftのエラーについてお話したいと思います。

Error型ではなくて、誤作動しそう、理想的コードパスではないもの

f:id:niwatako:20170303175336j:plain

  • 安定的、正常に動作するものを求めている。常にそうでありたい
    • 改善や新機能を早く出せる
  • 予期せぬ不具合から復帰して影響を最小限にしたい
    • DBからユーザーの一部が読めない時、諦めてクラッシュしたくない。
    • ユーザーがエラーを避けられない場合は特に注意したい
    • 起動してビューを表示するのは一つのよくある例ですが、自動で同期するなどの処理ではなおさら大事になります。ひょっとするとDoSのような状態に陥るかもしれないです。
  • サポートに電話して、再インストールしてくださいというのも、大事なデータを失うかもしれません

ただしコードを書く

維持できる、テスト可能なコードをどう書くか、誤作動しないように。

今日は簡単に入力について話します。

エラーを防ぐにはすべての入力を扱える必要があります。

  • 明示的
    • 読み手に目立つ、意図を明示的に伝える
    • 関数パラメータ
    • self(暗黙的に渡される引数、まだ明示的な部類と言って良いでしょう)
  • 暗黙的
    • 状態

明示的な例

f:id:niwatako:20170303175651j:plain

暗黙的な例

f:id:niwatako:20170303175708j:plain

状態、グローバル変数、Singleton。関数がこれらに依存すると、インプットは同一の明示的インプットでも、結果が異なります。これらに依存しないものはピュアファンクションといいます。

ピュアをfunctionに指定できるようにしたいという要望もSwiftに寄せられています。

状態

f:id:niwatako:20170303175825j:plain

ある時間で実行されているコード 命令形コードの順番によって定義される。 暗黙的だが明示的にすることも出来る。コメントで、〜の前に呼ばないと大変、など。 間違った順番でコンパイルできないようにコードを書くことができます。

f:id:niwatako:20170303175927j:plain

print文を独立して実行していきます。

スレッドが異なったパターンで降り混ざるに連れ、実行順序の状態が増幅します。

f:id:niwatako:20170303175959j:plain

3つの外リームに別れました。3の階乗パターンが有ります。

同時並行なしの例

f:id:niwatako:20170303180032j:plain

トップダウンの順番になっています。

あたいの一部は他の関数で呼ぶために引数として呼ばれている。

関係がコンパイラによって矯正されている。

1行目にlet c を置けません。aの値がないからです。

関数の呼び出しが何故ここにあるのでしょうか。入出力がありません。

  • リアル世界の状態
    • ファイルシステムやネットワーク
    • 以前のコードの実効出力
      • バグっていたコード
      • 忘れられたしよう

皆さんのコードに起因している。負の数以外を格納していると想定したが、過去のすべてのバージョンでそうだったか。誰かが-1やnullを入れて同期が取れていないことを示そうとしなかったか、バグが有ったかもしれない、今後アウトプットデータ上で実行できるのか。 ユーザーが直接弄ったデータを扱えるようにしておきたいか。

こういった状態の変更は無計画にやるべきではない。

きせかえ機能があります。

f:id:niwatako:20170303180326j:plain

今使われているきせかえテーマの設定を移動させる。設定はUserdefaultに格納されている。コードを追加して、Update後初めて起動した時に取り出して新しいフォーマットで格納するということをしました。しかしすぐ問題があることがわかりました。

アプリ起動時はDefaultテーマで、立ち上げると一つ前のテーマに戻ってしまいます。

テーマのサブシステムが私のコードの前に初期化されてしまいました。

でも次からの起動では移行したデータを読み出してきちんと起動してくれました。

曖昧な依存関係に起因している。

f:id:niwatako:20170303180523j:plain

一つのやり方は、2者の関係を肩を使って明示的にすることです。

方のインスタンスがないなら入力としてその方を必要とする者を呼び出せない。

特定の状態への遷移を表す方を用意し、その型を受け取ることで、その型を前提とする定義ができます。

f:id:niwatako:20170303180630j:plain

状態が発生した時にこのインスタンスを作って渡せばよいということになります。

少し話題を変えて、Swiftの実際のError型の話をしたいと思います。

f:id:niwatako:20170303180708j:plain

f:id:niwatako:20170303180723j:plain

大抵はOptionalが正解になります。

f:id:niwatako:20170303180803j:plain

よく知られる例としては壊れたDBにアクセスしたときやIOが落ちたときです。エラーを捉えてなんとかしようよりは、Happyパスを書くことに向いています。

私が思うに殆どのSwiftプログラマはカーソルを後戻りさせたりthrowしたりは嫌がると思います。

errorはそんなに頻繁に起きないのでいちいち考えたくないということ織田と思います。

でも起きると結構酷いのでその時の処理をabortすることになります。

なのでその時どうするかちゃんと設定する必要があります。do {} catch{}を使うことになります。そんなに頻繁に起きる・起こすものではいでしょう。

f:id:niwatako:20170303180959j:plain

早めに戻ってエラーをログすることになります。でも大抵はこれを見るとDBから来る、当然正常処理されると、OptionalのUnWrapを利用するんです。そこでthrowを付けました。

こちらの場合だと、サーバレスポンスを処理するようなものです。このような理想的状況がない場合、API利用者が最終的にOptionalに変換することになると思います。

Errorを使おうということにしたのでここには密結合が有りますね?

Error型はErrorハンドリングがその型と密結合している時に有用ということがわかります。

全体のコンテキストを知りたい。このような状況がError型が適している状況。

f:id:niwatako:20170303181243j:plain

コードのどこを明示的にモデリングしないといけないか

エラー入力や入力値を見落とすことがなくなるでしょう。

無関心ではいられないようなコードを見つけましょう。コードを堅牢に書く。

Optionalは情報量が少ない発生頻度がやや起こりがちなエラーに使いましょう。

Errorは情報量が多い場合や稀、複数の箇所から起きうる、コンテキスト依存のとき

f:id:niwatako:20170303181410j:plain

Q&A

たとえばFatalErrorを使う状況はどうしようもない、クラッシュしか無い物を使うことにどう思いますか

具体例がないと説明が難しいが私のコードでは!を使うことも有りますね。 ただ、単に無視するよりはどちらかと言うと探っていきたい、何故そういう状態になっているか。

(Resolve?Result?を使っているかとか?通訳に入っていなかったので質問不明)

Resolvを使っています。一部コードに最初のサーバーレスポンスとか。でもあまり機能性が高いかというとそうではないかなと思っています。もしでんぱさせるなら、もっとファンクショナルなアプローチになるかもしれません。