今回も引き続き、個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」を眺めていきます。その中の「get する時までには必ず set する」場面の話が今日のテーマになりますけれど、稀に必要に迫られてはさまざまな解決策を模索する、思いのほか難しい状況のように思います。どの手が最適かも都度都度違ってくると思うので、この機にいろいろな場面を想定して知識の引き出しを増やしておけたらいいなと思います。よろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #271
00:00 開始 00:30 前回の再学習 01:42 宣言・初期化・代入・参照 03:21 非オプショナルな定数の利用を意識していく 04:37 昔の人ほど変数を使いがちかもしれない 05:30 今回のテーマ 06:55 初期化時に値が決まらないとき 08:07 初期化フェースで値を決める努力は大切 08:45 初期化の時点では値を決められない例 12:21 フレームワークに従うのもひとつの解決策 13:37 UIKit は嫌われがち? 16:02 オプショナルにするのをやめる 16:45 遅延初期化を活用する案 17:23 preconditionFailure は取り除かれない 18:30 初期値はイニシャライザー実行前に採用される 20:19 初期値の初期化タイミング 22:34 クラスにおける初期化順番 23:44 既定イニシャライザーの暗黙呼び出し 25:31 初期値とイニシャライザーの違い 27:38 どこに不安を覚えるかは次回の話題 29:07 クロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #271
では、今日も引き続きSwiftコードの書き換えについてお話しします。今回も田中亮香さんのブログを読んで、いろいろな価値観や考え方について触れてみたいと思います。
今日の話題は「物理を保つような話題」、つまり、データの一貫性を保つためのアプローチについてです。具体的には、「ゲットするまでには必ずセットする」ということの重要性についてです。これは、参照する時点で必ず値が初期化されているプロパティを指しており、特にSwiftを使う際には非常に重要なポイントです。
ちょっと表現が曖昧かもしれませんが、要するに、変数に3つのフェーズがあることを意識すると良いということです。これをおさらいしましょう。変数のフェーズは、宣言(デクラレーション)、代入(アサイン)、参照(リファレンス)の3つに分かれます。Swiftでは、参照する前に必ず初期化されている状態を確保することが求められます。
特に、var
を使うと値がいつでも変わる状態になるため、できるだけlet
を使用して定数化することが安全性の向上につながります。しかし、let
は初期化が必要で、一度初期化すると再設定できません。そのため、初期化がまだできない状態のときにvar
やオプショナル(Optional)を使いがちですが、これを避けてlet
を使うことが重要というお話です。
他のプログラミング言語を経験していると、ついvar
やヌルポインタ(null pointer)を使ってしまうことがあります。しかし、Swiftでは可能な限りlet
を使い、値を確定させるよう心がけるべきです。
今日はこのあたりにしておきますが、このテーマは前回も少し触れましたね。同じ価値観で、「参照する時には必ず初期化されているインスタンスプロパティ」が重要だという話です。これは、Swiftだけでなく、プログラミング全般においても大事な原則です。
次回はもう少し広いスコープで値が決まらないときの対処法について深掘りしていきます。なるべくlet
を使う気持ちを持ち続けることが、コードの安全性と可読性を保つために重要です。
ここまでにしておきます。続きは次回またお話ししましょう。 あるインスタンスを作成した時点で、そのインスタンスのプロパティにはまだ何も入っていない場合がありますが、これを使うタイミングでは必ず初期化されているという話ですね。プログラムを組む側の発想でいうと、あるインスタンスを生成する時に、イニシャライズフェイズの段階ではまだプロパティを確定できないけれど、その時までには確定できるということです。要するに、イニシャライズフェイズが終わったら初期化できる、そういった話です。
例えば、struct
に何かバリューがあって、var
として、ここで適当な例として「hex」とします。この変数がイニシャライズの中で決まらない場合があります。端的に例を挙げると、大体は決まるのですが、稀に決まらない場合もあります。この「決まる・決まらない」も、まずは考えていくと、安直すぎると決まらないことがよくあります。しかし、突き詰めて考えればイニシャライズの中で決まることも普通にありますので、そのあたりは大事なところです。
例えば、func prepare
のようなメソッドがあって、このメソッドの中でややこしい処理の末にXが決まるようなコードがあったとします。このような場合でも、func reset
のようなメソッドがあり、このメソッドでXをリセットする機能を持っている場合、Xを計算するのが複雑だったりすると、位置を渡す時にもgetX
を使いたくなることがあります。しかし、このgetX
はself
に所属しているので、self
のプロパティ、つまり保存型プロパティをすべて初期化するまでは呼び出せません。これはカスタマイズフェイズに移行できないためです。
そこで、オプショナルにして既定値nil
で初期化した後にgetX
を呼び出せばうまくいくと考えるかもしれませんが、これをやめようという話です。この方向性はとても大事で、必ずイニシャライザーを実装している時には持っておきたい考え方です。
ブログ等ではどんな提案がされているかということに関してですが、例えば、コレクションビューはビューコントローラーのイニシャライズ段階ではまだ準備できないが、viewDidLoad
の中でコレクションビューを使うようになります。これはUIキットのライフサイクルの都合上、問題ありません。しかし、コレクションビューがイニシャライズ段階ではまだ決まっていないため、オプショナルにしていますが、これをやめようという話です。
このような場合、思いっきりIBOutletに喧嘩を売っているような話にもなりますが、IBOutletを使わずにインスタンスを入れているためです。このあたりはUIキットをもう少し活用する考え方が、個人的にはすごく出てくるのですが、それぞれですね。 個人的には、これをIBOutletにしてストーリーボード内にUICollectionViewを設置したほうがいいかもしれません。もしくは、オブジェクトとして持っておいてリンクしても良いでしょう。いずれにせよ、そのほうがスマートなコードになる気がします。しかし、ここでは何らかの事情でコード内でイニシャライズしないといけないようです。
ストーリーボードは嫌われることもありますが、個人的には、ストーリーボードの流儀に従うとアドバピューがやりやすくなり、おすすめです。しかし、最近の流れでは脱ストーリーボードを考えるべきかもしれません。時代の変化と共に、ビューコントローラーをコードでインスタンス化する方針に戻ることもありますが、完全に戻るわけではありません。巡りながら進んでいく感じがありますね。
さて、話を戻しましょう。オプショナルにして使う時に強制アンラップ (!
) するのは、過剰なnilチェックになりがちなので良くないという意見があります。シンプルな解決策としては、個人的には強制アンラップを使用するのが一番スマートだと思いますが、ここではレイジーバーン (lazy var
) の提案がされています。
lazy var
については、2つの問題があると感じます。まず、プレコンディションフェイラー (preconditionFailure
) ですが、これはどんなオプティマイズをしても取り除かれません。前提条件をハードコーディングして、その条件から漏れた場合にエラーを発生させるために使うものです。このコードは少し違和感がありますが、オプショナルをやめたいのであれば、lazy
にする必要はないかもしれませんね。
とにかく、まずはこのコードを動かしてみましょう。次のようにして試します:
var x: Int = {
if someCondition {
return someValue
} else {
preconditionFailure("条件に満たない")
}
}()
print(x)
これを動かすと、preconditionFailure
が発動するはずです。イニシャライザーが入る前に初期化されるので、その時点でエラーが発生します。クラスでも同じ動作が見られるでしょう。
以上が、この場面の議論の要約です。引き続き、他の方法やコーディングの進め方についても考えていきましょう。 宣言のフェーズでエラーになる点については変わりません。フィルタリングしてプリントするのが良いかもしれませんが、ランタイムエラーが発生するため、その場合プリントは実行されません。この点は重要です。
初期化について説明しますが、例えば初期化後にパスが表示されるとします。当たり前ですが、初期化が完了してからパスが表示されるのは、フェイタルエラーが発生する前にプリントされるためです。こうした流れで、イニシャライザーに入る直前まで初期化が行われます。
もう少し具体的な例を見てみましょう。親クラスのイニシャライザーを呼び出す場合、例えば以下のようなコードがあるとします:
class Parent {
var value: Int
init(value: Int) {
self.value = value
}
}
class Child: Parent {
var extraValue: Int
init(value: Int, extraValue: Int) {
self.extraValue = extraValue
super.init(value: value)
}
}
このコードでは、extraValue
の初期化が完了した後にsuper.init
が呼ばれることになります。
Swiftでは、あるオブジェクトが他のメソッドやプロパティにアクセスする前に、すべてのストアドプロパティが初期化される必要があります。もしこれが行われていない場合、初期化フェーズでエラーが発生します。
また、暗黙の呼び出しについてですが、デフォルトイニシャライザーは暗黙的に呼ばれます。例えば以下のようにデフォルト値を設定した場合:
class Example {
var y: Int
init(x: Int) {
y = x
}
}
ここで、y
の初期化は暗黙的に行われます。また、初期化フェーズが終わった後にsuper.init
が呼ばれることになります。
コンストラクターとイニシャライザーの違いについても触れておきますが、他の言語(例えばJava)ではコンストラクターと呼ばれるものが、Swiftではイニシャライザー(init
)と呼ばれます。Swiftの特徴的な点として、イニシャライザーのデフォルト実装が行われることもあります。
class CustomClass {
var x: Int
init(x: Int = 0) {
self.x = x
}
}
ここで、x
にデフォルト値が設定されているため、初期化時に特に値を渡さなくても正しく動作します。しかし、注意が必要なのは、宣言時に値を代入する場合、イニシャライザーが複数存在する場合など、パフォーマンスに影響がある可能性がある点です。
重要なのは、イニシャライザー周りの設計と実装に慎重を期すことです。書き方によってはパフォーマンスに影響を与える可能性があるため、適切な方法を選択する必要があります。
次回は「レイジー」の使用についてさらに深く理解することを目指します。これで本日の内容は以上です。お疲れ様でした。ありがとうございました。