https://youtu.be/ywKS3tXjEns
引き続き オプショナル
の具体的な特徴について眺めていきます。今回は オプショナル
の初期化時における特徴ですとか、値がないことを表す nil
の使いどころや特色みたいなところを観察していきますね。よろしくお願いします。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #144
00:00 開始 00:38 オプショナルの既定の値 02:28 確定初期化や didSet との兼ね合いは? 07:02 プロパティーラッパーや型に所属させることでは確かめられなそう 10:03 確かめられないことを確認してみる 10:13 プロパティーラッパーで検証できないことの確認 11:36 いわゆる Never 相当の型はプロパティーラッパーとして使える? 18:56 中間言語で初期化の有無を確認する方法もありそう 19:37 型に所属させる方法で検証できないことの確認 20:36 型の初期化はイニシャライザーの実行までで完了する 22:35 中間言語で初期化の有無を確認してみる 27:48 オプショナル型な変数の確定初期化はできないらしい 30:10 クロージング ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #144
はい、じゃあ始めていきましょう。今日は、引き続きオプショナルについてお話しします。今、画面に表示されているのは、前回のお話で大事なところです。特定の条件から値がなくても動作する必要があるときに、オプショナルを使うという話でした。今回はオプショナルの規定値について見ていきます。
Swiftでは、変数に値を代入するとき、その参照前に初期値を指定しておかなければならないという特徴があります。しかし、オプショナルの場合には、特定の条件において初期値を指定しなくてもいいという特徴があります。この点について、今回は詳しく見ていこうと思います。
例えば、var answer: String?
と書くだけでも使えるというのがあります。知っている人は知っているし、知らない人は知らないというものですが、実際にどうなるのか見てみましょう。
var answer: String?
普通は初期値を設定しないと使えないけれど、こうやって参照することができます。実際に結果としては、nil
が表示されました。つまり、値を代入しないでそのまま参照すると、nil
が入っているものとして扱われるという動きです。
print(answer) // 結果は nil
では、オプショナルでない場合はどうなるのか見てみましょう。
var answerNonOptional: String
// 初期値を設定しないとエラー
この場合、初期値が設定されていないため、エラーが発生します。このように、オプショナルにすることで初期値がなくてもnil
として扱われるというメリットがあります。
ここで、didSet
についても少し触れておきます。didSet
は変数に値が代入されたときに実行されるものです。初期化フェーズでは動作しません。グローバル変数やモジュールトップの変数に対してdidSet
を使うことで、代入フェーズを確認することができます。
例えば、次のように使います。
var answer: String? {
didSet {
// 代入フェーズで実行される
print("Value was set to \\(answer)")
}
}
このdidSet
は、変数が新しい値に代入されたときに実行されます。初期化フェーズでは実行されないため、初期値と代入の違いを判別するのに役立ちます。
最後に、Swiftではオプショナルを使うことで、特定の条件下で初期値を指定しなくてもいいという便利な特徴があることを理解していただけたと思います。今日はここまでとしましょう。次回も楽しみにしていてください。 そういった状況で、didSet
を使おうとすると初期化しないといけないということになります。具体的には、変数がnil
で初期化されている場合、didSet
が動作するということですので、仮に初期化フェーズにおいてnil
が代入されることで、didSet
がその代入に応じて動作しているという形になります。
言語仕様上、オプショナル型でないと理解しやすいかもしれません。didSet
が存在する場合、初期化が必要というエラーメッセージが出ます。これにより、初期化フェーズをしっかりと書いてあげる必要があります。初期化フェーズを明確に書いておけば、その後の処理は単純になります。didSet
が動作し、初期化フェーズが走ります。こういった仕様から、初期化フェーズを宣言時に必ず明記しなければならず、オプショナル型の場合にはnil
で初期化されることになります。
そして、didSet
が確認できると、この変数が適切に動作していることがわかります。もしdidSet
が存在しない場合は確定初期化が適用されますが、初期化フェーズがどのタイミングで発生するかが不明瞭になることがあります。
初期化フェーズを確かめる手段として、いくつかの方法が考えられますが、具体的にどういった手段が適用できるかをもう一度考え直してみると良いかもしれません。たとえば、オプショナル型に対してnil
の代入を省略した時点で、どのタイミングでnil
が代入されるのかを確認したい場合は、初期化フェーズの動作を詳細に追跡するための検証が必要です。didSet
を利用して初期化フェーズを明示的に確認したかったのですが、これがうまくきちんと検証できる方法を再度考えなければならないですね。
他にも考えられる方法はありますが、もっと具体的なテストコードを書くか、デバッグを活用するなど複数の手段を使って確認するのが良いでしょう。 プロパティラッパーには2つのアプローチがあります。1つはプロパティをラップすること、もう1つはオブジェクトに所属させることです。
プロパティラッパーの場合、ラップする裏で動いている型で初期化が行われるため、最初の代入式の有無に関わらずチェックができます。一方、型に所属する場合、nil
を代入するタイミングをdidSet
を使って先送りすることができます。しかし、イニシャライザーが終わった段階で初期化が完了するため、このタイミングでnil
が入ることは容易に想像できます。初期化フェーズの中でnil
がどのタイミングで入るかを調べる上で、どちらの方法もそれほど変わらないことに気づきます。
具体的にコードを書きながら検証してみます。まず、試しにプロパティラッパーを定義してみます。例えば、以下のように列挙型を使ってみましょう。
@propertyWrapper
struct MyTypeWrapper {
enum MyType {
case someValue
case none
}
var wrappedValue: MyType { get { .someValue } set { } }
init() {
self.wrappedValue = .none
}
}
このように定義すると、プロパティラッパーが構築できます。そして、イニシャライザーも追加して以下のようにします。
init() {
wrappedValue = .none
}
一方で列挙型を使っていると、never
型に近い動作を想定することもできます。例えば以下のようにします。
enum NeverType {
// 列挙型に何もケースを持たせない
}
この定義により、インスタンス化が実質的に不可能になります。ここでもし関数の戻り値としてNeverType
を返すと、
func someFunction() -> NeverType {
return NeverType() // 実際にはインスタンス化不可能
}
実行すると戻り値が得られないため、ランタイムエラーが発生することになります。
次に、トップレベルではサポートされていないプロパティラッパーをオブジェクトやクラスの中に持たせる方法も試します。例えば、以下のようにクラスに持たせてみます。
class DataClass {
@MyTypeWrapper var myProperty
}
このクラスをインスタンス化しようとすると、以下のように動作することがわかります。
let data = DataClass()
print(data.myProperty)
ここまでで、イニシャライザーで初期化が正しく行われる場合、代入時にエラーが発生することが確認できます。
以上のコードを通して、どちらの方法を取っても初期化フェーズでのnil
の処理が確認でき、特に大きな違いはないことが理解できました。 とりあえず、ここは動くんですよね。ここにはケースを入れましたが、これを消すとランタイムエラーが発生します。このランタイムエラーが起こるのは、Never
相当のタイプ、つまりケースが存在しないからです。なので、プロパティラッパーのインスタンスをインスタンス化しようとした時点でエラーが発生するわけです。
21行目でこの状況をきちんと検出できるのは素晴らしいですね。どのようなプログラムを組めばこんなに賢いものが作れるのか、ちょっと想像がつきません。イニシャライザーが実行できないということをここで予測して、原因を辿ってここにたどり着いているのかもしれませんね。
それはそれとして、予感が的中したので話を戻しましょう。要は、Never
型ではなく、適切な方法があるわけです。 要するに、インスタンス化できる型をプロパティラッパーで作れば、プロパティラッパーが正常に機能します。例えば、以下のようなコードを書きます。
@propertyWrapper
struct MyWrapper {
private var value: Int = 0
var wrappedValue: Int {
get { value }
set { value = newValue }
}
}
初期値を指定していない場合には、初期化が完了し、コードを実行すると正常に動作します。データに対して何もパラメータを渡していない場合、それは26行目の初期値を使用するという意味です。わかりにくいかもしれませんが、イニシャライザーを定義すれば、プロパティラッパーのイニシャライザーがパラメータなしで定義されているため、既定値として使用されるのです。
struct MyStruct {
@MyWrapper var myProperty
}
これにより、オブジェクトの初期化時に自動的にプロパティが初期化されます。ただし、オプショナル(Optional)を使用する場合、そのものにnil
を入れるタイミングを制御するのは難しいです。例えば、次のようなコードでは、nil
は予期しないタイミングで設定されることがあります。
struct MyStruct {
@MyWrapper var optionalProperty: Int?
}
この場合、初期化フェーズと代入フェーズの違いを理解するために、didSet
プロパティオブザーバーを使用して確認する方法もあります。しかし、イニシャライザー内でどのように値を設定しても、イニシャライザーが終わった時点で必ず初期化が完了しています。
struct TestStruct {
var value: Int {
didSet {
print("value was set")
}
}
init() {
self.value = 0
}
}
var test = TestStruct()
test.value = 10
上記のコード例では、value
プロパティに対して値を設定したときのみdidSet
が動作します。初期化時には動作しません。
また、中間言語を解析する方法で初期化タイミングを知ることも一つの手段です。これにより、どのタイミングでnil
が設定されるかを見ることができます。ただ、すべてのケースにおいて確認が難しい場合もあります。追加の手法として循環参照のチェックも試みることが考えられますが、詳細な検討がさらに必要です。 とりあえず、多少のミスは気にせず進めても問題ありません。ここではメイン関数について見ていきます。オプショナルについても触れますが、UnsafeMutablePointer
が出てきますね。これはInt32?
をUnsafeMutablePointer
として解釈します。また、Optional
型の使い方も見ていきます。たとえば、%3
がオプショナルなInt
です。
まず、enum
のOptional
を確認します。Optional.none
という書き方があるようです。この場合、%4
にnil
を代入するという形になります。これにより、nil
がオプショナルな変数に代入されます。このグローバルアドレスに関してもオプショナルですが、今回はあまり深く考えないようにします。重要なのは、その概念を理解することです。
次に、インスタンスに0を入れたときの挙動を見てみます。先ほどのOptional.none
を代入する操作がどうなるか試してみます。たとえば、test2
にnil
を入れてみて、結果がどう変わるかを確認します。中間言語の最適化前でも、ちゃんとオプショナルが代入されていることがわかります。最適化するとどうなるかも実験します。
ここでdo
ブロックを使って取り扱います。特化しないパターンと特化するパターンで比較します。同じ行で宣言と初期化をしない場合、nil
が代入されることになります。つまり、ローカル変数でもオプショナルプロパティには注意が必要です。宣言と同時に初期化しないと、デフォルトでnil
が代入されるのです。これは0を入れるのと同じ意味です。
最適化された中間言語を見てみると、オプショナルのデフォルトが入っています。このことから、パフォーマンスを気にする場合は宣言と同時に初期化を行う必要があります。そうしないと、nil
がデフォルトで代入されてしまいます。
では、いい具合の時間になったので、今日はこれで勉強会を終わりにしましょう。お疲れ様でした。ありがとうございました。