https://youtu.be/a46zROcqyns
今回も引き続き オプショナル
の具体的な特徴について眺めていきます。前回は nil
に照準を絞って観察していったので、今日はその続きから、初期化周りの特徴といった オプショナル
を取り巻くその外回りに視野を広げて観察していきますね。よろしくお願いします。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #143
00:00 開始 00:20 今回の展望 00:45 nil リテラルと型推論 01:53 オプショナルではない変数に nil は割り当てられない 02:10 オプショナルを適切に使用していく 04:32 適切にレスポンスの値を扱っていくとしたら? 05:54 オプショナルを変数で使う場合と戻り値で使う場合の印象の違い 07:09 値か状態か 08:20 定数で nil を扱うときの感覚 09:41 詳細を伝えたいときはエラーハンドリング 11:37 エラーを投げない関数に付けた try は警告扱い 14:49 return と警告の扱い 18:42 コールバック関数で結果を受け取るパターン 19:56 クロージャーの命名規則 21:48 並行処理を使って結果を返す方法 22:19 必要のない場面でのオプショナルは避ける 23:31 ExpressibleByNilLiteral 24:32 nil とは? 27:27 解説の粒度の匙加減 29:47 既定イニシャライザーを用いた既定値表現 30:54 静的プロパティーを用いて表現する方法 31:21 4つの表現方法を見比べてみる ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #143
はい、では始めていきますね。今日も引き続きオプショナルについて見ていきます。オプショナルって、延々と見ていけそうな感じがとても面白いですね。
今日はどんなお話から始めましょうか。前回は主に nil
についてお話しましたが、途中から雑談で盛り上がったので、 nil
についての話を全部仕切れていないかと思います。なので、今回は少し振り返りつつ、先へ進んでいく感じになります。
nil
の話をしている中で型推論や、 nil
だけでは情報が足りないことについてお話しました。このスライドで「サーバーレスポンスコード = nil」というコードが成り立つのは、前の行でサーバーレスポンスコードの型が明記されていて、それが Int
のオプショナル型だということがすでにわかっているからです。そのため、 nil
に対して何も追加の情報を加えなくても、 Int
型のオプショナルとして nil
リテラルが解釈されるのですね。こういった部分が大事なところです。この辺りをずっとイメージできるようになるなら、結構 nil
というコードのリテラルを理解していることになるのかなと思います。
スライドに書いてあるのは、「オプショナルではない定数や変数に nil
は割り当てられない」ということですね。こうした当たり前のことも、前回はお話していなかった気がします。そして、コード内の定数や変数が特定の条件下で値がなくても動作する必要があるときは、適切な型のオプショナルな値として宣言することが重要です。
特定の条件下で値がなくても動作する必要があるとき、これがとても大事なところです。「特定の条件下で値がない可能性がある時」という言い方をすると若干違和感を感じるかもしれません。説明しないとその違和感がどこから来るのかはっきりしないですが、通用性が大事なのです。
「全体としてちゃんと値がなくてもプログラムが動く必要があるとき」そういう広いときに、適切な型のオプショナルな値として宣言することが大事です。適切な型のオプショナル型、つまり Int
型のオプショナルのような感じです。
例えば、サーバーレスポンスコードを考えると、これが特定の条件下で値がなくても動作する必要があるときは、 Int
にしておくべきでしょう。オプショナルを使うほうが適切ではないかと思います。 nil
が入っていたらまだレスポンスを返していない、というような意味合いになってくるのだと思いますが、それは考えすぎかもしれません。
例えば、サーバーレスポンスコードを Int
のオプショナル型として nil
と書くよりは、ファンクションとしてリクエストを送り、レスポンスコードを返すようにするほうがいいかもしれません。そして上で何かしらのレスポンスを返した後、サーバーレスポンスコード = リクエスト、のようにして、仮にレスポンスを返してこない場合があるなら、そのときオプショナルを使うのが良いでしょう。
このように、定数にオプショナルを使う場面と、戻り値としてオプショナルを使う場面では意味合いが違ってきます。戻り値として使う場合、戻り値がある・ない、つまり戻り値が取得できた・できなかったという意味合いになりますが、変数に対してオプショナルを使う場合、 Int
型の値の時もあれば値がない状態もあり得るという表現になります。
こんな感じで、値が固定されているのか、時間や状況によって変化するものなのかということが表現できます。こういったふうに理解していくと、オプショナルの使いどころや意味がわかりやすくなるのではないかと思います。 要は、この結果としての値なのか、時間によって変化していく状態なのか、そういった意味合いの違いが、変数に型を割り当てるのと、戻り値として割り当てるのとで違ってくる感じが自分はします。実際のところはわかりませんが、そんな感覚の違いも意識してみると、オプショナルの使い心地というか使い方に関して何か感じ取れることができるかもしれません。それが感じ取れると、また使い方が丁寧になってくると思いますし、自分も意識してみると良いと思います。
ここがリード(let
)だとやっぱりおかしいわけです。状態が完全に固定されてしまいますので、もう少し柔軟な状態にしたい場合、たとえば「値がないこともあればあることもある」という状態を完全に考慮する必要があります。つまり、nil
を含めて変数として扱うということになるんです。var
のときのオプショナルと、let
のときのオプショナルは感覚が違う気がします。どのような違いか整理できていない部分もありますが、ここは注意深く考える必要がありますね。nil
と他の値が対等である必要がある気がします。
このように、let
でオプショナル型を使う場面について考えてみると、また違った視点が見えてくるかもしれませんね。適切な例が出せればよかったのですが、難しいところですので、ここは一旦話を切り上げます。
さて、話を戻します。サーバーからレスポンスがあったかどうかを返すときにオプショナルを使うことができます。もっと精密な状況を得たい場合は、Result
型やエラーハンドリングを活用するのも一つの手です。レスポンスコードがそのまま表現できるのであれば、必ずしも複雑なハンドリングは必要ないかもしれません。ただ、状況によってはそうとは限らないこともあります。クローズとなるとエラーコードを返すことになりますが、正常な場合は、もう少し具体的なレスポンス、たとえば具体的な値を持つものを返した方がスムーズに進行するでしょう。
この発想から、レスポンスに対して特定の型を用意して、その中でコードをInt
型として持たせてあげるという方法もあります。その場合、エラーハンドリングをthrows
にするか、普通の戻り値として扱うかの選択肢が出てきます。こういった設計によって、レスポンスコードをInt
型のオプショナルとして用意する必要がなくなります。
ふと思ったのですが、throws
が付いていない関数に対してtry
を付けると警告が出ますよね。これがエラーではダメだったのかな?と思います。try
を付けたコードがリリースコードに含まれることを考えると、警告レベルで済むよりもエラーにした方が良いのではないでしょうか。catch
ブロックに入る場面を想定しなければ、エラーではない状況が生じることは異常です。したがって、APIの使い方を間違っている場合は、エラーで止めるべきだと感じます。
まとめると、throws
が付いていないのにtry
を使っているエラーは警告ではなく、もっと厳密にエラーとして扱った方が良い場面があると思います。これは、オプショナルの使い方にも言えることかもしれません。いずれにせよ、使い方に注意を払いながらコードの安定性を維持することが重要です。 なるほど、これが使えるやつですね。使うんですよ、普通に。確かに、このノリに似てる気もしますね。でもね、些細なところではありますが、エラーで止めてくれることもどこかにあったんですよね。忘れちゃいましたが、他に警告というのなら、例えばリターンを変えちゃったときに、その先は行きませんよってことがありましたよね。
今、完成じゃないので、ここに書けばいいのかと考えると、これでいいなと思います。そうすると、10行目にはいきませんという警告が出ますね。エラーがどこだったか、確かに何かであったんですよ。それは警告でも通せそうだけど、エラーにしておいてくれているという状況です。思い浮かぶ人いますかね。例えば、Void型の関数でリターンを変えたときなど。
いや、それはコンパイルが通った気がしますね。Voidとして書かなくてもいいですが、リターンでVoidも書けるし、リターンで行を変えてプリントみたいなことをすると警告が出ることがありますね。ここは警告です。戻り値としてプリントを実行したいのか、リターンを変えたから15行目は実行されたくないのか、パースの問題が出てくるんです。Voidの場合に限っての警告、エラーだったかもしれません。
以前にもこの勉強会でエラーと警告の使い分けについて話したことがあるけれど、ちゃんと話ができなかった気がしますね。公式のドキュメントに何かないかなと調べてみても、今ちょっと出てこないです。スウィフトのドキュメントの中に、エラーと警告はどういうふうに使い分けるか、どんなふうなメッセージにするかのルールがあった気がするんですよね。そのあたりもう1回探してみましょう。
エラーと警告の差を意識してみると、面白いところが見えてくると思います。エラーでもいい気がするけど根拠が見つからない。同じように警告でいいんじゃないかなと思いますが、そのほうが根拠も見出しやすいかもしれません。
あと、仮にレスポンスが出るかどうかという状況が8行目で想定されています。結果がまだ出ないときには、レスポンスを戻り値として返すのではなく、コールバックを使う書き方に変わってきます。戻り値としてレスポンスを受け取り、何か処理をしてもらうという書き方ですね。これでコールバックを呼び出して、設定していくという形です。
先ほどの8行目で思い出したのですが、APIデザインガイドラインではクロージャーのパラメータにラベルをちゃんとつけるようにという記載がありました。しかし、途中からクロージャーの仕様が変わって、ラベル名をつけられなくなったんですよね。 API的には目に見えるようにラベルを付けて省略して書くというのが推奨されるので、APIを書くときにはそのように書くと良いでしょう。コード自体は変わらないのですが、ドキュメントの見やすさに関わってくる部分です。
リクエストコールバックのような形式だと見やすくないかもしれませんが、定義を見れば分かりやすいでしょうし、QuickHelpにも出てきます。ラベル名を添えつつ、外部ラベル名は省略する場合もあります。呼び出すときに外部ラベルが省略されているため、何も書かなくても良いです。
もう一つの方法として、値が返ってくるまで保留のような方法があります。コールバックではなく、純粋にリターンを使うという形です。これにはいろいろなやり方があるので、オプショナルを使う必要がない場合にはオプショナルを避けるという考え方が基本になります。
特定の条件下で値がなくても動作する必要がない場合には、適切な手段を使ってその状況を作る方が安全性が増します。オプショナルの値があるかどうかを検査するコードが不要になるため、コードを書く負担も減ります。このようにすることで全体的に良い成果が得られるでしょう。
オプショナルを使う際には、必要があるのかどうかをよく考えることが大切です。特に慣れないうちには、一度手を止めて考えてみると良いでしょう。
また、興味深い点としてオプショナルではない定数や変数にnil
は割り当てられない場合もあります。適切な例として、例えばバランス計算のときに、nil
という表現を使いたい場合があります。ExpressibleByNilLiteral
を使うことで、nil
を扱うことができます。
struct Balance: ExpressibleByNilLiteral {
var amount: Int
init(nilLiteral: ()) {
self.amount = 0
}
}
このようにすれば、コンパイルが通り、オプショナルではない独自定義の型でもnil
を扱うことができます。これにより、表現力が広がるので、適切に活用すると良いでしょう。 初心者向けの本としては、割り当てられないって書いておいたほうがいいと思います。初心者に「実は違うんだよ」とか言い始めても困っちゃうことがありますよね。そういえば、昔自分のブログに「T++の話題の中で、自分のブログはマニアックな解説が少なくてとても良い」とコメントをもらったことがありました。自分もなかなか価値のあるところはあるけれど、T++のブログを書く人たちは専門性が非常に高いブログを書いていますよね。逆に、私はあまり専門性の高いブログは書けないので、そういったところもあるのかなとは思います。要は、そういうノリですよね。専門性が高すぎると初心者向けにはなかなか合わないことがあります。
例えば、今度はオプショナルの話を少し具体的にすると、本では「オプショナルではない」と一言で言い切ってしまうこともありますが、それがかえって初心者には難しく感じることがあるのです。入門書を読むときは、そういった部分にも注意を払う必要があります。
例えば、プログラミングを始めたばかりの頃に「この本はすごく良いな」と思った愛読書があったとしましょう。今はだいぶステップアップした自分が昔の本を読んだときに、細かい部分がどう書かれているのかを意識して読んでみると、自分の成長が感じられるかもしれませんね。そういった視点で読み直してみるのも面白いです。
また、nil
を使う方法について別のアイデアもあります。例えば、ゼロを表現するときの方法の一つにイニシャライザーを使って初期値をゼロにする方法もありますね。ちょっとオプショナルとは離れますが、イニシャライザーの話です。これで、例えば balance = 0
とすると、デフォルトイニシャライザーを使う方法になります。状況によりますが、これもゼロという状態を表す一つの方法です。
考えうる方法としては他にも、スタティックな定数として nil
を使う方法などがあります。どれが良いかは状況や型の意味合いなどにもよりますので、必ずしも「これが一番」というものはないかもしれません。しかし、ディフォルトイニシャライザーも悪くはないと感じます。
ディフォルトイニシャライザーは規定値という意味合いを持つので、例えばバランスがゼロのときが規定かというと、それも状況によりますよね。数字でもゼロを規定とする考え方もありますし、場合によっては1が最適なこともあります。こういった部分を考えると、どれが最適かは一概には言えないところもあります。
nil
を入れる方法だと、オプショナルと被ってしまう感覚があります。値がないかもしれないという感覚を含むため、これはちょっと良くないなとも思います。ゼロを入れる方法はとてもニュートラルな表現です。ゼロだなという感じもあり、これは悪くない気がします。
この部分は特別な状況として、nil
という値が24行目にはうまく表現されている気がします。それぞれ一長一短があり、比較してみるのも面白いですね。どれが一番良いかはお勧めしづらい部分もあるので、いろいろと試して考えてみるのもいいと思います。
今日の勉強会はこれで終わりにしましょう。お疲れさまでした。ありがとうございました。