https://youtu.be/jVFhkRsGCdc
今回は オプショナル
の初期化時における特徴についてもう少しばかり窺ってから、引き続き オプショナル
の特色みたいなところを見ていきますね。特色についてはこれまでのおさらい的な話題になりそうな感じですけれど、どうしてそういう主張が本でされているのかみたいな視点で眺めていくのも面白そうな予感がします。よろしくお願いしますね。
——————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #145
00:00 開始 00:32 前回の、オプショナルな変数の初期値に nil が設定される話 01:22 オプショナルな変数で確定初期化は行われない 02:20 確定初期化 04:30 確定初期化の定数への適用 06:13 オプショナルな定数には既定値が設定されない 07:45 オプショナルの既定値から窺う変数と定数の違い 09:26 プログラマーの意図を確かめるための仕様? 12:57 変数と定数の次元的な違い 15:23 実装から観測する値と状態 22:29 オプショナルでの確定初期化のまとめ 23:54 オプショナルに関する補足事項 24:24 Swift の nil と Objective-C の nil とは異なる 26:07 ポインターによる表現の話 27:41 Swift は構造体も nil 表現可能 30:08 ポインターの捉え方についての話 31:47 クロージングと次回の展望 ———————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #145
はい、では始めていきますね。今日はオプショナルについてお話しします。既に何度かオプショナルの話をしていますが、今回もさらに詳しく見ていきます。
まず、前回お話しした内容の振り返りから始めましょう。少しややこしい話でしたので、再度確認しますね。Swiftのオプショナルは、初期値を与えないと自動的にnil
が設定されます。オプショナルな変数に初期値を与えないと自動的にnil
が設定されるというのは、知っている人にとっては当然のことですが、そうでない人にとっては聞き逃しやすいポイントです。
この仕様により、コードを書く手間が少し楽になります。前回の要点としては、nil
を明示的に代入しなくても、変数の宣言時点でnil
が代入されるということでした。
Swiftの特徴として、変数は初期化を遅らせることができます。これを「Deferred Initialization」と言います。この仕様はオプショナルと非常に似ています。しかし、オプショナルな変数に関してはDeferred Initializationは適用されず、宣言時点で自動的にnil
が代入されます。
Deferred Initializationは、最終的に変数が使用されるまでに初期化されていれば、コンパイラがそのコードを通してくれるという仕様です。そのため、初期化する際に不要な初期値(例えば、とりあえず0
を入れる)を代入する必要がありません。
オプショナルは似たようなコードが書けますが、その時点で存在しないことを示すためにnil
が入ります。これにより、値がない状態をコードで書きやすくなります。また、状況に応じて後で値を入れることもできます。特定のシナリオでは=
を使わずにnil
が設定されていることが有用です。
Deferred Initializationについてもう少し話しましょう。変数宣言でvar
を使いましたが、一般的にプロパティはlet
で宣言されます。これは、初期化の遅延によって、後で状況に応じて変わる初期値を持つことができるが、それが定数であることを示すためです。このように、初期値を後で決定しつつも、定数として扱うことができるのがこれの面白いところです。
今回はオプショナルおよびDeferred Initializationの特性を活用するコードの例を紹介しました。理論だけでなく、実際にコードを書く際にはこれらの特性を賢く利用してください。また、余力のある方は、今日の内容を覚えておいてください。
では、続きの内容に進みます。 話をライドに戻しまして、ここで非常に大事な点があります。それは、オプショナルな変数に規定値を与えないと自動的にnil
が設定されることです。ただし、オプショナルな定数に規定値を与えないと、自動的にnil
が設定されないんです。
これ、面白いですよね。つまり、何が言いたいかというと、例えばvar value: Int?
をプリントするとnil
になります。これは、nil
が自動で代入されているからです。しかし、これがlet
の場合はどうなるかというと、エラーになります。「定数のvalueが初期化前に読まれた」というエラーです。つまり、変数の場合はnil
が自動で入るのに、定数の場合はnil
が入らないという仕様なんです。
これは面白くないですか?何も考えずにオプショナルにnil
が代入される仕様を考えると、let
に対してもnil
が入るようにしたくなりそうな気がしますが、そうはなっていません。ですので、let
の時にはnil
を代入したいなら、明示的にnil
と代入する必要があります。
この違いを見てどう感じますかね?ちょっとその違いを並べて書いてみましょう。例えば、let value: Int? = 2
と、var anotherValue: Int?
で、後者は何も書かない。この2つを比べてみると、一見してアンバランスな感じがします。ただこれは意図的な仕様です。
こういう設計には設計者の信念が込められていることが多いです。その信念を理解することが、設計の意図や背景を読み解く上で重要です。 これを考えると、人の気持ちになって考えることによって、多くの新しい視点が見えてくるものです。そして、人それぞれの見え方や感じ方が異なるため、それぞれの人にとっての正解も異なってくると思います。このような異なる見解を取り入れることが大事です。それぞれの信念を確かめていく過程が重要なポイントだと思います。
コメントでいただいたお話により、間違いを減らすためのアプローチにも様々な見かたがあります。その間違いを減らす方法自体にもいくつかの枝分かれがあるのではないかと思います。たとえば Swift で let
を使う場合ですが、let
は値が一度設定されるとそれ以降は変更できません。仮に let
に nil
を代入したとすると、それ以降変更することができないという点で、const
に近い概念になります。
let
を使うと、最初に代入された値を変更することができず、仮に nil
を代入したのであれば、その後は nil
として扱うことになります。これに対して、「本当に nil
として扱いたかったのか?」という疑問が出てくるかもしれません。ただ、これがエラーの一因ともなり得るわけで、Swiftではそれをエラーとして検出してくれる点がメリットです。
一方、var
の場合、後で値を変更することが可能です。最初は何も値が設定されていなくても、後で値が確定した時点で var
に値を代入することができるため、特に問題はありません。nil
はニュートラルな状態として捉えられるため、この使い方も合理的です。
こうした面から見ると、let
と var
には大きな違いがあります。let
は定数として、値が一度設定されたら変更できないという点で、厳密さが求められます。これに対して var
は変数であり、値が後から設定・変更可能という点でより柔軟です。これらの違いは、プログラムの設計や実装においても大きな影響を与えるものであり、それぞれの特性を理解して使い分けることが重要です。 今後、時間の経過に伴って値が変わる可能性がある、つまりある時点ではこういう値だけど、別の時点では違う値になる、そういう時間軸が加わっている感じが分かりますか?これは常識かどうか分かりませんが、とにかく変数は時間の流れも含む非常に線的な世界観を持っています。
これに対して、定数のlet
はその逆で、値が変わらないという特性を持っています。この違いが、オプショナルの既定値を入れるかどうかといった設計上の分岐点にもつながってくるわけです。どちらが繋がっているかはケースによりますが、こういった違いが1行目と2行目、特にオプショナルの価値観からも読み取れます。個人的に、こういう違いを理解するのは好きですね。
例えば、変数のvar
は値を都度更新して変わるのに対し、定数のlet
は最初の値を保持し続けます。この感覚を持つことで定数と変数の使い方が、「書き換えるから変数」「一定だから定数」といった単純な使い分けを超えて、どう設計すべきかという洗練された理解につながると思います。
具体例を挙げると、例えば温度計のクラスがあったとして、温度を示すプロパティをDouble
型で定義します。イニシャライザで温度を設定し、例えば横浜の温度を取得する場合、温度ボタンが押されたときにその温度を取得して表示する、といった処理が考えられます。その際に取得した瞬間の温度は定数let
として扱い、ここから先は変わりませんが、表示されたものはその後の状態の更新を持つため変数var
として扱います。
例えば、次のように実装できます。
class Thermometer {
var currentTemperature: Double
let initialTemperature: Double
init(initialTemperature: Double) {
self.initialTemperature = initialTemperature
self.currentTemperature = initialTemperature
}
func updateTemperature(newTemperature: Double) {
self.currentTemperature = newTemperature
}
}
このように、現在の状態を表すものと、その瞬間の値を保持するものとで別々に管理することができます。これが定数と変数の違いを活用した例です。このような微妙な違いを理解することで、プログラム設計がさらに面白くなりますよね。 Swiftの言語仕様を見ると、とても細やかで行き届いている感じが伝わってきますね。こうした部分を見ると、安心して使っていけるなと思います。
さて、ここでは「let」と「var」の違いについて話します。例えば、nil
を代入しないとコンパイラが警告を出す場面がありますよね。逆に、nil
を代入する場合はエラーが発生しません。この動きに関して前回の内容を踏まえて、オプショナルの初期化について確認します。
オプショナルの変数に値を入れるとき、2行目にnil
が暗黙的に代入され、その後4行目で10
が代入されます。つまり、2行目が初期化フェーズで、4行目が代入フェーズに該当します。確定初期化の概念もここに関わってきます。例えばオプショナルでない変数に対しても、同様の動きが見られます。3行目の= 0
が初期化フェーズで、6行目の= 10
が代入フェーズです。ただし、6行目の= 10
を省略すると、初期化フェーズに変わります。この動きが確定初期化となります。
一方、オプショナルでない変数では、var
の初期値= 0
を省略した場合、2行目が初期化フェーズとなり、その後代入フェーズが続きます。しかし、6行目を省略して変数を使用すると「初期化されていません」というエラーが出ます。ここが初期化フェーズの重要なポイントです。
さらに、オプショナルは初期化フェーズが必ず実行されますが、普通の変数はそうではありません。let
の場合の初期化フェーズが実行されるかどうかを確認するために、簡単なコードを試してみます。
let optionalValue: Int? = nil
print(optionalValue) // ここでコンパイルが通るか確認
このコードで、コンパイルが通ります。let
は一度しか代入できないため、初期化フェーズが正しく行われている証拠です。let
変数にnil
を代入すると、2回目の代入はできないためエラーになります。一方、var
の場合はnil
を入れても次の行で値を代入できます。
オプショナルのlet
については、確定初期化が行われます。このように書いても、普通のオプショナルではない変数と同様に使用できます。ただ、var
の場合は最初にnil
が代入され、その後コードがパスします。こうした違いを踏まえてオプショナルを使えば、より面白いコードが書けるかもしれませんね。
オプショナルと変数定数の組み合わせによる動きの違いは、非常に興味深いところです。この点を紹介しました。 オプショナルの初期値を与える特徴について見ていくと、本では1行だけの説明ですが、詳しく解いていくと面白い点がいろいろと見えてきますね。次にオプショナルについて補足します。『The Swift Programming Language』に記載されていた内容も参考にしながら説明します。ただ、この内容は今までのオプショナルの話と重複する部分が多いかもしれません。せっかくなので、どのような意図で書かれていたのかも考えながら改めて見ていきたいと思います。
まず、Swiftのnil
はObjective-Cのnil
とは異なる構造を持っています。Objective-Cのnil
はヌルポインタで、Swiftのnil
はある型の値が存在しないことを表現するリテラルです。これは概念的な存在であり、Objective-Cのnil
は存在しないオブジェクトへのポインタです。Objective-Cではnil
がゼロの値であることを意味していましたが、Swiftでは任意の型のオプショナルに対してnil
を使用できます。これが概念的なものであるため、伝えることが重要になります。
ポインタという言葉がどれくらい今浸透しているのかはわかりませんが、若い世代ではポインタという言葉自体を知らない人も出てきているかもしれません。時代が進むにつれて、"無い"ということの表現が抽象化されてきました。今では、より中小化された形としてnil
リテラルが存在します。
昔、オブジェクトがポインタだった時代には、ヌルポインタで存在しないことを表現していましたが、その方法は複雑でした。例えば、nsインテジャー
というプロパティに何もない状態を表現するのは困難でした。それを解決するための手法としてヌルポインタを用いたりしました。
Swiftでは、nil
という概念を抽象化し、オプショナルという概念を導入しています。これにより、例えばint?
やString?
のようにシンプルで明確なコードが書けるようになりました。これがSwiftの大きな利点です。構造体でもクラスでも同じようにオプショナルを使用できるのが特徴ですね。
ポインタに関しては、昔からプログラマーが一皮むけるための難所として解説がされてきました。ポインタを理解することはプログラマーにとって重要なステップです。例えば、int
型のポインタからさらにそのポインタへのポインタがあるといったような状況を理解するのは難しいですが、重要です。
時間になったので今回はここまでにしますが、オプショナルについては引き続き次回以降も見ていきます。次回は条件分岐と合わせて、実際にどのように使っていけるのかを見ていきます。今日はこれで勉強会を終わりにします。お疲れ様でした。ありがとうございました。