https://youtu.be/7NJZG416KpM
前回から オプショナル
を実際に使っていくための機能と安全性をにらんだ言語サポートについて眺めはじめましたけれど、その中で「すべてのインスタンスが nil
と比較可能」な場面に出会い、それがなかなか気になりました。おおよその様子は前回に話をしていて掴めたものの、どこか気になるところもあり、オプショナルの特質を窺う機会にもなりそうなので、今回は改めてそのあたりを眺めてみてから、本題の続きに移っていこうと思います。よろしくお願いしますね。
———————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #147
00:00 開始 00:18 今回の展望 01:12 非オプショナルな値と nil とを比較可能 03:23 非オプショナルと nil を比較できるのは自然にも感じる 07:04 辻褄を合わせるように実装されている印象 07:55 サブタイプとは 09:35 等価比較演算子の定義を探してみる 11:23 Never に対する等価比較? 11:51 オプショナル型に関する等価比較演算子 12:13 なぜ _OptionalNilComparisonType が対象の等価演算子が呼ばれるのか 15:26 型推論によって _OptionalNilComparisonType が選ばれる 20:43 NSObject は Equatable に準拠 24:38 全ての型を nil と比較できるようにするための策 26:43 全ての型を nil と比較できる必要性は? 27:49 クロージング ————————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #147
さて、オプショナルについての続きですが、実用的なところを見ていこうというつもりではあったんですけれども、前回の最後に「どんな型のインスタンスでも比較可能だよ」というところが気になりました。これはなかなか面白くて、今日はその辺りをもう少し突き詰めてみたいと思います。オプショナルの特徴を通して見えてくるものもあるので、それを一旦整理してから本題に戻るという形にしようと考えています。
少し問題となったところですが「問題」というよりは、「気になる点」と言った方が適切かもしれません。例えば、以下のようなコードがあったとします。
let value: Int = ?
これは型推論で Int
型となり、オプショナルではないので nil
にはなり得ません。ところが、if value == nil
という条件式が成立するというのが驚きでした。オプショナルじゃない値と nil
の比較が可能だというのが、非常に興味深い点ですね。なお、この部分は日本語のドキュメントでは「常にフォルスになる」とされています。
また、Int
型と Int
型のオプショナルを使って以下のような状況を考えてみましょう。
let v1: Int = ?
let v2: Int? = ?
この状態で v1
の比較が成立するのは自然なことですね。評価式に変更してみると、確かにプレイグラウンドでも結果が見やすくなります。v2
との比較も成立します。
オプショナルでない Int
型とオプショナルの Int
型の比較で一致判定をする場合、両方に同じ値が入っているから通ると理解できます。ですが、nil
という値が型が決まっていない状況で、Int
型と比較されたときに、どう型推論が働くかが気になるところです。Int
型は nil
と互換性がない型ですので、実際にはエラーが出ます。v2
については、右辺が nil
でも型推論的には左辺と合わせてくれるので、これは自然な比較です。
続いて、サブタイプの話に入ります。例えば、Int
型と Int?
型を比べたとき、Int
型が Int?
型のサブタイプとされるという見方ができます。なので、実は互換性のある型として解釈しているのです。自分としては、オプショナルではない型に対して nil
と比較するのが少しすっきりしない感じがあり、裏で実行時に合わせている部分がありそうだと感じます。
ここまでの話で、オプショナル周りで違和感や理解が追いつかないところがあれば、ぜひ質問してください。かなり複雑な話をしている自覚があります。特にサブタイプについて補足すると、以下のような形になります。
let value1: Int = 5
let value2: Int? = value1
このように、Int
型が Int?
のサブタイプとして位置付けられているので、自然に代入が可能です。これがよく使われる場面は、関数のパラメーターなどです。例えば、何かしらの型を取る関数があったとして、そのパラメーターにオプショナルでない Int
型の V1
を渡せるということです。これができないと、わざわざキャストが必要になります。
以上、オプショナルの比較について説明しましたが、もし意見があればぜひ共有してください。 Swiftの言語仕様において、サブタイプや型の一致の重要性について議論しました。また、透過比較の演算についても触れました。登場する演算について詳細に探るために、import Swift
を使用すると、Swiftモジュールの主要な部分が確認できました。この中で、==
関数がどのパラメータを取り得るかを調べ、その結果、オプショナルに関する部分を見つけました。例えば、オプショナル型の拡張において、ラップされている値が等しい場合に対応する形になっています。
さらに、Never
型が比較対象として用意されているのが興味深いですね。Never
型はインスタンスを持たないのに、比較演算ができるのが面白い点です。具体的に何が起こるのかはまだ検証の余地があります。
続いて、オプショナルのラップされた値に関する部分を見ていきます。ここでは、オプショナルのエクステンションとして、ダブルラップの比較が行われる形になっています。前回の中間言語を見た時にも、この部分に辿り着きましたが、次に興味深い点としてOptionalNilComparisonType
があります。
この型の定義を探してもすぐには見つからなかったため、GitHubのAppleのSwiftリポジトリを検索しました。すると、オプショナル型の標準ライブラリ内で、OptionalNilComparisonType
はExpressibleByNilLiteral
になっています。つまり、ニルからの変換を受け付けている型だということです。面白い点は、ニルからの変換は受け付けるものの、値を持つわけではないことです。
このように、Swiftの詳細な仕様や標準ライブラリの内容を探ることで言語の特性や設計思想を深く理解することができます。 型パラメーターが添えられていないという特徴があります。このオプショナルニルコンパリソンタイプは、パターンマッチングのエクスプレッションパターンの演算子や等価比較演算子に関して定義されています。ここまでの説明から言えることとしては、オプショナルニルコンパリソンタイプは特定の型として定義されていて、これはnil
から変換可能なのです。このようにインスタンスと任意の型とは互換性があるという動きが確かに存在します。
しかし、14行目が通る理由について考えると、比較演算子の右辺がニルコンパリソンタイプの等価比較で、左辺がオプショナル型となっています。右辺がオプショナルニルコンパリソンタイプであることを推論できることがすごいですよね。こういった推論が可能であること自体、非常に興味深いです。
例えば、ファンクションとしてsumsum
を定義し、片方の引数をオプショナル、もう片方をオプショナルニルコンパリソンタイプとすれば、以下のようなコードになります。
func sum(_ a: Int?, _ b: Int??) -> Bool {
return a == b
}
この関数に、例えばnil
を渡したとき、どのような挙動をするかといった動作検証も必要です。
オーバーロードした関数を考えると、次のようになります。
func sum(_ a: Int?, _ b: Int?) -> Bool {
return a == b
}
このようにした場合も、問題なく動作するかを確かめる必要があります。オプショナルニルコンパリソンタイプを使う意図は、任意のインスタンスに対してnil
と比較できるようにすることです。この仕組みを意識して設計されているのですね。
他にダブル型などのオプショナルを定義した場合にも、次のようにコンパイルが通るようになります。
func sum<T: Equatable>(_ a: T?, _ b: T?) -> Bool {
return a == b
}
これにより、ダブル型や文字列型など多くの型での比較が可能です。ただし、イコータブルでない型に関しては注意が必要です。例えば、FileManager.default
を渡す場合、次のように書くとどうなるかも検証します。
let fileManager = FileManager.default
この場合もイコータブルとして扱われるかがポイントです。NSオブジェクトプロトコルはイコータブルではないことがあるため、その点も注意深く調べる必要があります。イコータブルが特別な実装をされていないときは、インスタンスが一致するかどうかで判定され、カスタマイズする場合には==
をオーバーライドするルールが存在します。
以上のように、Swiftのオプショナル型や比較演算子に関して、詳細に理解することが求められます。 そう言えば、これだからできちゃうんだ。完全に独立させないといけないんですね。オブジェクトのように作ってあげたときに、このオブジェクトは渡せなくなることがあるんです。
例えば、イフ文でやりたいことがイコータブルではないオブジェクトの場合に、イフ文で let object = オブジェクト
として、if object == nil
のように書いたとします。これだとコンパイルが通ります。このように実際に評価式を見てみると評価が通るんですね。これをやりたいわけです。しかし、これだと先ほどの例でいうとエラーが出てしまいます。例えば、14行目がエラーになるのです。
演算子も関数と同等として言語仕様が作られているので、Something
が片方はオプショナルでない比較可能なもの、もう片方は nil
のパラメータを受け取るようにできなければ、==
演算子で同等のことができません。これを通すために Something
をオーバーロードします。片側がどんな型であっても、もう片側が nil
である場合、その型として受け取れるようにすることでコンパイルをパスさせることができます。例えば、38行目もコンパイルをパスする形で実装しているようです。
推論が通れば、例えば5行目の説明で片側が Int
型で、もう片側が nil
だった場合には、両方とも Int?
型のオプショナルであるとする推論ができるようになります。もしこれができれば、おそらく OptionalNilComparisonType
は不要でしょう。しかし、それができないため、これをできるようにするために OptionalNilComparisonType
を作り、それを使っています。
どんなオプショナルでないインスタンスと nil
との比較を可能にしています。これができるようにするための工夫です。例えば、数字と合わせていって、任意のオプショナルでないインスタンスと、nil
というパラメータをうまく処理できるようにしています。このようにいろいろ工夫して、オプショナルでないものでも nil
との比較が可能になっています。
ただ、ここまでする必要があるかというと、個人的にはコンパイルが通らないなら、それでも十分ではないかと思います。完全に比較できないオブジェクトなら、コンパイルエラーになってもよかったのではないか、と感じています。
今回の勉強会では、このように Swift はどんなインスタンスでも nil
と比較できるような仕組みを持っていることが理解できたかと思います。これを頭に入れておくとよいでしょう。
それでは、今日の勉強会はこれで終わりにします。お疲れ様でした。ありがとうございました。