https://www.youtube.com/watch?v=Xfyl6RuesXY
今回は Swift.org にある About Swift の中から NULL 安全 について眺めていきます。そんなテーマをとりわけ オプショナル型 に着目して眺めていく回になりそうです。よろしくお願いいたしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #10
00:00 開始 01:46 02:55 08:47 10:34 16:06 オプショナルバインディング 20:11 オプショナルチェイニング 21:40 オプショナルチェイニングと代入の併用 23:08 nil 結合演算子 23:58 CustomStringConvertible 25:53 Objective-C の nil と似た動き 27:40 オプショナルを支援する言語機能 35:21 Optional 型の定義 37:53 質疑応答 38:48 オプショナルな配列の扱いについて 46:26 ディクショナリーでのオプショナル型の扱いについて 53:15 クロージング ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #10
はい、じゃあ今日は「About Swift」を見ていますけど、それのいよいよ最後という感じですね。今回は安全性について見ていこうかと思います。ここが面白いところで、この面白さは人によって異なる場面かなと思います。「ヌル安全」と聞くと、魚が水を得たようにイキイキと話しだす人もいる分野です。特に詳しい人は言語の型システムに興味を持つので、いろんな興味深い話が聞けるところかと思います。
私はそこまで深入りせず、特に今回はSwiftの機能に注目します。安全性がどのように表示されているか、特にSwiftが具現化しているオプショナル型について中心に見ていきます。脱線してもらう分には構いませんけどね。では行きましょうか。
Swiftの大事なポイントの一つとして、Objective-Cや他のC系列のプログラミング言語と比べて大きく異なるのは、Swiftでは規定でnil(ヌル)を許容しない点です。nilを許容しないことで、コードが明瞭かつ安全になり、クラッシュの原因、特にプログラマーのミスでクラッシュする原因の発生を予防することができます。
このスライドは「About Swift」の中の「ヌル安全」について日本語で書いただけですが、これを見てみると話の流れが若干飛んでいる気がしますね。まず紹介しておくと、nilを作成または使用しようとすると実行時エラーを発生させる仕組みになっています。これが完全にObjective-Cと比べての話ですが、Cでも実行時エラーが発生しますよね。
例えば、何らかの変数がInt型であった場合、これにnilは入れられないということです。常識と言えば常識で片付けられる話ですけど、こういったことがまずできない。同様の仕組みは他のC言語系列でもありますが、例えば以下のようなコードがあります。
let object: SomeClass = SomeClass()
一般的な言語、C++とかJavaとかC#もそうだったかな、ではnilをオブジェクトインスタンスとして入れて、オブジェクトが何もない状態を作れますよね。Objective-Cも同様です。
NSObject *object = nil;
Swiftではこのように宣言しても、nilを許容しない型であればエラーとなります。例えば、以下のようなコードを考えてみましょう。
var myInt: Int? = nil
このように、Swiftではオプショナル型を使用してnilを許容することができます。このオプショナル型がSwiftの安全性を支える重要な特徴の一つです。 とりあえず意味が分かればいいや。
プラス型の場合、インスタンスがないということで nil を入れられる言語は結構多かったです。しかし、Swiftではそのあたりが厳密になっており、オブジェクト型であってもそのインスタンスがないという状況はそのままでは作れない感じです。これが大きなポイントになります。
具体的に何が大きなポイントかというと、何かしらのメソッドを持っているオブジェクトがあったとき、そのオブジェクトでメソッドを呼ぶことが許されているという点です。許されているというのは、ちゃんと実行できることが保証されているということが非常に大きなポイントです。
もし nil が許されている言語だった場合、オブジェクトのインスタンスが nil だったときには、nil エクセプションのようなランタイムエラーで落ちることになります。そのため、もし nil が入っている可能性があるならば、自発的にオブジェクトが nil ではないかをちゃんと調べて、そうだった場合に実行するなど、プログラマーが先手を打って対処する必要があります。これが nil が入るかもしれない場面の危険性です。
Swiftでは、nil を許容せずに設計することで、nil が入っていないことが保証されるので、そのような条件を気にせずコードを書けるようになります。これが大事なヌル安全のポイントだと思われます。
とはいえ、適切な場面では nil を扱うことが適切な場合もあります。そうしたときには、オプショナル型を使うことで nil を扱えるようになっています。このオプショナル型のことをSwiftでは革新的な機能と評価しています。実際にSwiftを触った特にObjective-Cから来た人たちは、このオプショナルを触ったときに素晴らしい機能だと感じることが多いです。
オプショナル型は何かというと、値として nil を持つことができ、その nil が入ってくることがあるという状況を想定したコードをプログラマーが書かなければならないという義務を与えてくれる存在です。ざっくり言うと、そんな感じです。
このあたりは実際にコードを見ると多分わかりやすいと思います。この値が nil である可能性を想定したコードを書くことを余儀なくされるのが非常に効果的です。今までの勉強会でも、try やエラーハンドリングの話をしたときに、try を書かせるのがすごいとか、throws を書かせるのがすごいとか、若干手を煩わせられるけど、その煩わされた以上にコードが書きやすくなったりする点を見てきました。オプショナルも全くそんな感じです。若干手間は増えますが、手間が増えたとは思えないぐらい効果的な機能です。
具体的にどういうことかと言うと、オブジェクト型にハテナマーク(?)をつけるとオプショナル型になり、nil も受け付けるようになります。そうすると、それが nil であるかを判定して実行するコードがもちろん書けるわけです。たとえば、以下のようにオブジェクトをオプショナル型にしようとするとします。
var optionalObject: MyClass? = nil
このように、シンプルにオブジェクトがオプショナル型であるにもかかわらず、普通に使おうとするとコンパイルエラーになります。
optionalObject.someMethod() // コンパイルエラー
そのため、この値が nil であるかどうかをチェックする条件をちゃんと書く必要があります。これがSwiftのオプショナル型の大きな特徴です。 これは大事なポイントです。これはnilが入ってくる可能性があるから、プログラマーがその状況をどう判断するかということが求められています。その対応をコードとして書かないと、コンパイラはエラーとして処理してくれません。
例えば、あるオブジェクトのインスタンスにnilが入っていないはずだという場合には、びっくりマーク(!)をつけてあげます。そうすると、nilが入っていないなら普通にアクションメソッドを実行します。もしオブジェクトがnilだった場合には、その時点でランタイムエラーが発生して終了します。Playgroundを今動かすと、おそらくランタイムエラーで落ちるでしょう。nilだからですね。
次に、オブジェクトがnilだった場合にどうするかを考えます。nilだった場合、この例ではランタイムエラーが発生します。今までのC言語ベースやC++のような言語でも当たり前の動きです。
Swiftで同じようなことをやろうとすると、このクエスチョンマーク(?)やびっくりマーク(!)を書く必要があります。確かにコードを少し多く書かなければなりませんが、これによってオブジェクトがnilになる可能性があるかどうかを確認できます。また、nilが入った時にランタイムエラーが発生する場所がわかるので、コードを読む人にとっても理解しやすくなります。
もしnilが入ってきたときにランタイムエラーが起きると困る場合は、オプショナルバインディングを使用する方法があります。例えば以下のように書きます。
if let unwrappedObject = object {
// オブジェクトがnilではない場合の処理
} else {
// オブジェクトがnilの場合の処理
}
上記のif let構文によって、オブジェクトがnilでない場合には新しい変数unwrappedObjectにその値が代入されます。この変数は非オプショナル型のオブジェクトとして扱われるので、以降のコードでは安全に使うことができます。
こうすることによって、余計なびっくりマークやクエスチョンマークを使わずに済みます。実際に動かしてみると、elseブロックが実行されます。もしXcodeが間違っている場合は再起動してみると良いでしょう。
Xcodeを再起動して、再度試してみると、今度は正常に動作するかもしれません。実際にはXcodeの勘違いだったということもありますね。 もしオブジェクトがnilの場合、ここが実行されます。これが、もしちゃんとインスタンスが入っているようだと、当然のように正しかった方のブロックが動いてアクションメソッドが実行されるという動きになっています。
例えば、elseブロックがない場合、nilが入っていなければアクションメソッドを実行するというようなコードを書くときには、オプショナルバインディングを使ったスタンダードな書き方(スタンダードというと少し言い過ぎかもしれませんが、一般的なコード)のように書けます。
他にも、「?」という記号があります。これを使うと、オブジェクトがnilじゃなかったときに限ってアクションを実行する、いわゆるオプショナルチェーニングというものです。これを使うと、例えばオブジェクトがプロパティを持っており、value = 100としたときに、プリントで値を表示しようとすると、もしオブジェクトがnilだった場合には無視されるという動きになります。結果としてはnilなので大した問題ではありませんが、オブジェクトだった場合にはちゃんと100が代入され、「オプショナル100」と表示されます。
もしvalueがnilだった場合、ニル結合演算子(「??」)を使ってnilだった場合には別の値に変えるという手法もあります。例えば、ディスクリプションプロパティを使う場合、これは一般的にそのインスタンスが表現するテキストを表示します。ディスクリプションプロパティはCustomStringConvertibleプロトコルに準拠していると、そのインスタンスそのものをテキスト表現する文字列を取得できます。
この場合、nilかどうかで名案が分かれ、nilじゃない場合にはディスクリプションが表示され、nilだった場合には「(nil)」という表示に変わります。
さらに余談ですが、このコードがObjective-Cのnilの動きと似ています。Objective-Cの場合、例えば以下のようにコードを書いたとします:
NSObject *object = [[NSObject alloc] init];
そして、オブジェクトに対してアクションメソッドを実行したり、プロパティに値を代入したりします。このとき、オブジェクトがnilだった場合、アクションメソッド呼び出しやプロパティへの代入が無視されるという動きになります。この「?」を使った演算子はObjective-Cのnilの動きと似ているので、Objective-Cを使っていた人にとってはわかりやすいかもしれません。これは余談ですが、とても面白い動きだと思います。 ここで大事なこととして、Swift には「オプショナル」という型があります。このオプショナル型を支援する言語仕様が組み込まれていることで、オプショナル型の変数が nil か nil ではないかというのを適切に制御し、nil だった場合の対応も含めたコードが作成できます。この仕組みによって、コードの完成が簡単になります。
オプショナル型については、すでに馴染みのある方も多いかと思いますが、例えば Swift にオプショナルがなかったと仮定して、同様の機能を自分で実装する場合について考えてみましょう。列挙型を使って、以下のようにコードを作成します。
enum OptionalValue<T> {
case empty
case value(T)
}
このように列挙型を作成し、例えば以下のようなコードを書いた場合に、
var data: OptionalValue<Int> = .value(10)
このコードは、値があるかないかを管理するものになります。
この列挙型が持っているアクションを実装しようとすると、例えば以下のようにします。
func setValue(_ value: T) {
self = .value(value)
}
ただし、このような実装は煩雑で、正しいコードを書いても面倒になる場合があります。例えば、値の判定や設定を行うために以下のようなコードを書いてみましょう。
switch data {
case .empty:
// 何もしない
case .value(let value):
print("Value is \\(value)")
}
また、別の条件分岐として以下のような処理が考えられます。
if case .value(let dataValue) = data {
data.setValue(100)
}
これらのコードは、一見シンプルに見えるかもしれませんが、nil を含むか否かを考えることは大変な作業になります。しかし、Swift ではオプショナル型を使うことで、以下のように簡単に記述することができます。
var optionalData: Int? = 10
オプショナル型の定義は、実際には以下のように標準ライブラリで作られています。
enum Optional<T> {
case none
case some(T)
}
このように、ジェネリクスで型パラメーターを取る列挙型として定義されています。このため、オプショナル型を使った場合、上述のような複雑なコードを書く必要がなくなり、簡単にnil を確認しながら安全にプログラムを書くことができます。
このオプショナル型とオプショナル専用の構文を組み合わせることで、nil を考慮したコードが安全かつ簡単に書けるようになっていることが、Swift の大きな魅力の一つです。
オプショナルに関しては、まだまだ多くの機能が用意されていますので、オプショナルセクションが後ほど出てきたときに詳しく見ていこうと思います。オプショナルに関する疑問や気になる点があれば、今後のセッションでも自由に質問してもらえればと思います。
以上がオプショナルの基本的な話になります。 もし物足りない点があれば、どの辺が気になるでしょうか?オプショナルの取り扱いについて煩わしいと感じる点があるかもしれませんね。
例えば、オプショナルの配列を使っていて、ちょっと面倒だなと感じることはありませんか?ここで問題となるのはオプショナルの配列です。具体的には、Int型のオプショナル配列です。まず、変数を宣言しましょう。
var values: [Int]? = nil
この場合、値が最初はnilです。配列自体がオプショナルであり、中身はオプショナルではないという前提です。ここで、配列に最初の値を入れるときが少し面倒ですね。
例えば、もし配列が空っぽだった場合、以下のように初期化しないといけません。
if values == nil {
values = []
}
values?.append(1)
このように、各所で初めて値を追加するときにこの処理を書かないといけないのは少し手間です。
この手間を減らすために、willSetを使ってみるのも一つの手かもしれません。ただ、willSetでvaluesがnilだった場合に空配列にすることが可能か試してみましょう。
var values: [Int]? {
willSet {
if newValue == nil {
values = []
}
}
}
しかし、この場合でもうまく動かないことがあります。プロパティラッパーを作る方法もありますが、それも手間がかかりますね。
もう一つのアプローチとして、拡張機能を使ってみましょう。オプショナルの配列に拡張機能を追加して、appendのようなメソッドを自分で定義することができます。
extension Optional where Wrapped == [Int] {
mutating func append(_ newElement: Int) {
if self == nil {
self = []
}
self?.append(newElement)
}
}
これにより、valuesがnilでも以下のように安全にappendを行うことができます。
values.append(10)
ただし、このような拡張機能はAPIデザインガイドラインに必ずしも沿っていない可能性があるため、どのようなアプローチを選ぶかは検討が必要です。
確かに面白い課題ですね。こういった工夫をすることがSwiftの魅力でもあります。 頑張るのはおすすめできないなと思いつつも紹介したり。あとは、そうですね、ソフトチェーン、イニシャライザー、オプショナルラッピング、オプショナル... 何か他にあったかな。
オプショナルについてですね。確かに面倒ですね。これは面白そうですね。オプショナルは結局 Any の中に含まれていて、そのバリュー(値)が Any のディクショナリーに対してサブスクリプション(購読)でやるときと初期化するときの動きが違うところで、毎回どれがどれか悩んだりしますね。
「オプショナルエニー」はこういう感じですか?
はい、そうですね。これでサブスクリプション、サブスクリプションって言ったらこうですよね。このときに Any が取れる。もう一つは初期化するとき、すなわち let x: Int? = nil というケースですかね。初期化しようとすると、ここが nil の場合。例えば Int の配列などですね。nil のままはダメでしたっけ? Any だと nil だと浮いちゃうから何の nil かを表現しないといけないですね。
確かに、Any にオプションがあると、使ったことはないけどできますよね。こういう感じなら nil がダイレクトに入りますけど、ここはややこしい。こういうことですかね。キーが消えると nil を入れるか。これどう動くんだっけ?記憶が間違っていなければ、上の方がキーが存在して値が nil。下の方はキーが削除されます。
これは怖いコードですね。
そうですよね。サブスクリプトだから、バリューに対してですもんね。
怖いです。プレイグラウンドが動かなくなっていますけど、つまりオプショナルも怖いときは怖いということですね。面倒なときは面倒だし、怖いときは怖い。だいぶ良いんですけどね。これはすごく好みな例です。こういうところをニラネコ(細かく見ると)いろいろ面白いんですよ。
ちゃんと実行ができないな。落とせば動くか、多分予想通りに動くと思うので、違ったらまた補足しますけど。最初の例、ここがすごく気になります。これではやり方全然ダメだったということですけど、ひらめく人いますかね。何か面白い方法がありそうな気はする。nil だったら初期値を入れて何かしらメソッドを作らないとダメですかね。プロパティラッパーかね。
さっきの例、ちゃんと動いたので、キーがあるまま nil のタイプとないもので素晴らしい。この例、個人的に大ヒット。ディクトの方、これオプショナルが二重になっているのかな。データというか Any?? ね。これ一個 ? をつけるとダメ。一個の ? はダメか。ピクリマーク(強制アンラップ)しないとダメか。これは当たり前でした。書いていけば当たり前でした。
なるほど。これちょっと自分の課題にしてみよう。nil だった場合だけ何かする。面白そう。計算型プロパティ一個作って入れ込んどくっていうのも一つですけどね。private な変数として、ちょっとここ今動かないですけど、オブジェクトの中とかに作ってね。バッファーを作って。これはオプショナル、これもオプショナルでいいのか。パブリックの方で、バリューを作って。ここでね、ゲットのときに何をするとか、セットのとき何をするとか。ここでね、バッファーが nil だったらみたいな。スタンダードなコードで実現していくとこうかもしれないですね。あまりスッキリしないんで、もっといい方法を考えたいです。
オプショナル型の nil 安全という話をしておきながら、個人的には面白い例が出てきたおかげで、まだ安全になりきれていない感が出てきましたけど、基本的には nil 安全、結構うまく作られていると思います。
説得力なくなったね。オプショナルは結構奥が深いところで面白い。慣れれば慣れるほど Swift がサクサクとかけていくし、さっきあげていただいたコードもオプショナルに慣れ親しんでるから、こういうところがウィークポイントになるねっていうのが見えてくるっていう成果なんでしょうね。
こんな感じで一応オプショナルの紹介という形で。時間になったので、今日はこれぐらいにいたしましょうかね。1時間お疲れ様でした。ありがとうございました。