https://youtu.be/QVQPmU4E6co
これまでに オプショナル
の概念的なところを眺めてきましたけれど、今回からはそれを実際に使っていくための機能、安全性をにらんだ言語サポートについてみていきます。Swift に触れている人にはお馴染みの初歩的機能ですけれど、せっかくのこの機会に普段なかなか振り返らないそんなところに意識を向けてみれたらいいなと思っています。よろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #146
00:00 開始 00:11 今回の展望 01:09 条件文とオプショナル 02:53 nil との等価判定 04:33 計算型プロパティー利用時の注意 05:40 オプショナルに対する等価判定の挙動 06:27 オプショナルを switch で判定する 07:23 オプショナルをパターンマッチで判定する 08:31 列挙型としての判定はそれなりに複雑になる 09:21 オプショナルのための特別な計らい 09:44 型パラメーターと付属値を同じ名前にすると? 10:52 独自の型では常に nil ではないと判定される 12:59 オプショナル型に対する等価比較演算の挙動 16:56 nil と互換性のない型とでも比較可能 18:10 呼び出し先を中間言語から探ってみる 22:04 nil リテラル対応の独自型を nil と比較する際の注意 24:35 型を nil リテラルに対応させるなら比較可能性も検討する 27:13 _OptionalNilComparisonType 28:05 比較可能なものを扱うオプショナル型の等価比較演算 ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #146
では始めましょう。今日はオプショナルの実用的なところを見ていきます。今まではオプショナルの基本的なコンセプトとその存在意義に焦点を当ててきましたが、今日は実際にSwiftでオプショナルを使う方法を見ていきます。Swiftを既に使っている方なら当然のように使っている部分もあるかもしれませんので、気軽に進めていきたいと思います。
まずは、if
文を使ってオプショナルをどう判断するかを紹介します。オプショナルが値を持っているかどうかをnil
との比較で判断することができます。例えば、comparativeNumber != nil
という形で判断します。この方法はSwiftの言語仕様によってサポートされており、意識を向けてみると面白い部分です。
具体的なコードで見てみましょう。==
演算子や!=
演算子を使うことで、オプショナルの値がnil
かどうかを判定することができます。値があるときはnil
と等しくないと見なされます。以下のような例を考えてみます。
var randomValue: Int? {
return Bool.random() ? 0 : nil
}
このように、randomValue
というプロパティをランダムでnil
または0
を返すようにしておきます。次に、if
文を使ってこの値がnil
かどうかを判定します。
if randomValue == nil {
print("The value is nil")
} else {
print("The value is not nil")
}
このコードは、randomValue
がnil
であれば「The value is nil」と表示し、nil
でなければ「The value is not nil」と表示します。
ここで注意が必要なのは、計算型プロパティを使う場合、参照するたびに値が変わる可能性があることです。以下のように一度値を格納してから判定する方が良いでしょう。
let value = randomValue
if value == nil {
print("The value is nil")
} else {
print("The value is not nil: \\(value!)")
}
このように、一度変数に格納することで、判定と表示で値が一致しないという問題を回避できます。
オプショナルの存在を判定する方法として、!= nil
と== nil
の両方を使うことができます。以下のようにelse
ブロックを使って逆の判定も可能です。
if value != nil {
print("The value is not nil: \\(value!)")
} else {
print("The value is nil")
}
このように、オプショナルの有無を確認する基本的な方法をおさらいしました。それでは、この応用例をプレイグラウンドで試してみましょう。 何気なくできるように見えますけれど、言語仕様で特別な計らいがされていることによってこれができるんです。ここが結構大事なところになってきます。
まず、Int
のオプショナルに関してですが、それはリアクショナリーと言われることがあります。オプショナルは「列挙型」であるというのがSwiftの有名なポイントです。つまり、ケースとしてnone
とsome
があり、some
のときには値が関連値として所属しているという仕組みになっています。本来、オプショナルを判定するときには列挙型なので、列挙型の判定をしなければならないわけです。そのため、switch
文も使用できます。
一般的には、switch
文でケースとして判定し、例えばnone
だったらprint("Value is nil")
と書きます。そして、some
の場合にはlet
で値を取得し、print("Value is \\(value)")
というように表示します。本来の列挙型に対する判定方法です。
他の判定方法もあります。例えば、パターンマッチングをif
文で使用し、if case .none = optionalValue
のように書きます。もし値がnone
ではなければ、value
に代入し、print("Value is \\(value)")
と表示します。nil
じゃないときには、このif
文の条件部分が実行されます。このようにして、!= nil
に相当する判定を行います。逆に== nil
にしたい場合には、case .none
とマッチさせます。このときにはprint("Value is nil")
とします。
こうやっていろいろと書いていくとイメージがつきやすいと思います。列挙型がそのまま判定されるため、値を持っているかどうかを判定するのが少し複雑になります。ですが、言語が特別な計らいをしているので、!= nil
という判定でオプショナルがnil
でないかどうかを簡単にチェックできるのです。
独自の型で同じことができるかどうかは別の話です。例えば、オプショナルと同等の列挙型を作った場合、次のように定義できます。
enum MyOptional<T> {
case some(T)
case none
}
これに対して、以下のように判定することができます。
let value: MyOptional<Int> = .some(42)
switch value {
case .some(let val):
print("Value is \\(val)")
case .none:
print("Value is nil")
}
それでも、MyOptional
独自の判定方法を導入しなければなりません。このように、Swiftのオプショナルは単なる列挙型以上に特別な計らいがされている部分が多いのです。
例えば、次のように書くと、何が起きるか試してみます。
let value = MyOptional.some(42)
print(value == nil)
これは動かないと思われます。MyOptional
は単なる列挙型なので、Swiftのオプショナルと同じようには動きません。この点が、言語仕様で特別な計らいがされているかどうかの違いです。
この特別な計らいによって、Swiftではオプショナル型を簡単に扱うことができるようになっています。 物が出ているのは構いませんが、コンパイルエラーにならないか確認してみましょう。そのため、一旦ここで止めましょう。後で使うかもしれませんので、コードを以下のように変更してみます。
let x: Int? = 10
if x != nil {
// 何が起こるのか確認しましょう
}
これで x
はバリューを持っていますね。それから nil
であるかを判定しているわけです。イントとして扱ってみましょう。
let y: Int = x!
if y == nil {
// 通常はここには到達しません
}
これは条件としては成立しないはずですね。つまり、 != nil
として判定しているというわけです。ここまでの理解では、 x
が nil
ではないと判断してすべて実行されることになります。
さて、 == nil
がどう解釈されているかを見てみましょう。この定義は func ==
のように記述されるはずです。例えば、以下のような形で定義されているかもしれません。
func ==<T>(lhs: T?, rhs: T?) -> Bool
この ==
がオプショナルとして解釈されている可能性があります。このため判定が複雑になっているのです。
さらに検証するため、以下のようにコードを書き換えてみます。
if x == nil {
// ここに到達するかもしれません
}
今回はオプショナルが関与していますが、これを除去してみましょう。
let x: Int = 10
if x == nil {
// 通らないはずです
}
これでコンパイルエラーが出るか確認してみます。オプショナルな型である nil
を含む場合は特別な扱いが行われているようですね。
一般的に、オプショナル型は nil
と互換性を持つため、こちらのコードはコンパイルされません。
もし具体的なエラーメッセージを確認したい場合、オプショナルの定義に関する詳細を追跡するのが良いでしょう。例えば、Playgroundやナビゲーション機能を利用してデフィニッションを確認できます。
この一連の流れを見ると、Swift のオプショナル型が特別な計らいを受けているのが分かりますね。個人的には nil
と互換性がない場合にコンパイルエラーが出て欲しいと思いますが、現行のSwiftの仕様ではそのようにはならないようです。
まとめとして、 == nil
の判定はオプショナル型の特殊な判定として実装されています。この部分を理解することで、Swiftの言語仕様について深い理解が得られるでしょう。 中間言語の詳細な部分まで触れるのは、踏み込みすぎるのではないかと思います。言語仕様については、もっと高レベルで両面からアプローチしていきたいと感じています。例えば、変数 バリュー
を 0
に設定して、 if バリュー == 2
というコードを書いて、その中間言語としての出力を見てみましょう。
この例でテストとして、Swiftコンパイラの中間言語(LLVM)を使ってみるのは良いですが、出力にエラーやワーニングが出ることもあります。具体的には、Swiftのオプションで swiftc -emit-ir
を使うと、中間言語を出力できます。この際、もしワーニングが出たら、それを見て修正する必要があります。
メイン関数には、リテラル変換や比較が含まれ、その中で ==
演算子が使われます。これはインフィクスオペレーターであり、オプショナルやニルの比較が絡んできます。このコードは、オプショナルに対応するために、NILリテラルが使われる場合があります。その場合、値がオプショナルになり、比較がうまくいかないこともあります。
この問題への対策として、まずバリュー型を ExpressibleByNilLiteral
プロトコルに準拠させ、init(nilLiteral:)
を実装する方法があります。ただし、この方法でも完全に動作しないことがあります。その原因は、オプショナルの比較の特性にあります。
バリュー型が Equatable
と ExpressibleByNilLiteral
の両方に準拠していないと、ニルリテラルが適切に解釈されません。オプショナルなバリュー型との比較が正しく行われるためには、 Equatable
プロトコルとニルリテラル対応の両方を実装する必要があります。そうすることで、ニルリテラルがバリュー型として解釈され、比較がスルー(通過)されるようになります。
独自に定義したバリュー型に特別な計らいが行われることはなく、演算子のほうで特別な計らいが必要となります。この辺りは少し複雑かもしれませんが、知っておくと、Swiftでのプログラミングがより理解しやすくなることでしょう。 この言語仕様についてですが、昔も同じようにできたかはちょっと記憶が曖昧です。以前は両辺が同じ型であるというのがSwiftの安全性のコンセプトだったと思いますが、オプショナルが絡むと少し事情が変わってきますね。こういった特別な計らいはありがたいですが、ルールをしっかり理解していないと正しいコードが書けなくなることもあるので注意が必要です。
どのくらいの頻度でExpressibleByNilLiteral
を適用するのかは分かりませんが、これを書いたことがある人はどれだけいるのでしょうか。自分は一度使ってみたくて書いたことがありますが、このプロトコルを使う際にはEquatable
にも適用できるかどうかを考えないと少し不安があります。Equatable
がなければnil
との比較ができなくなるので、これは当然といえば当然ですが、注意が必要ですね。
オプショナルがどのように作られているのかを見てみると、ExpressibleByNilLiteral
になっていることが分かりますが、比較可能かどうかはエクステンションで実現されているようです。例えば、比較検査として以下のようなコードがあるかもしれません。
if let value = wrappedValue, value == otherValue {
// 比較ロジック
}
Optional
はnil
との比較を詳細にもっていますが、アンダースコアで始まる型が使われているので、これは内部的な実装のあたりでしょうね。左右両方に適用されるタイプが実装されています。
このようにオプショナルの比較は複雑ですが、元の概念としてはOptional
でもEquatable
のラップされた値が等しい場合に限って==
を使うようにしています。特別な計らいで中がEquatable
でなくても、ラップされた値とnil
との比較ができるようになっています。
時間になったので今日はこの辺で終わりにしますが、次回またこの内容を補足するかもしれません。引き続きオプショナルを便利に使える機能について見ていこうと思います。お疲れさまでした。ありがとうございました。