今回はせっかくなので、前回の最後に駆け足で紹介した 幽霊型
の仕組みをゆっくりとおさらいする回にしてみますね。そのあとで、その発端となった気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」の「あとから必ず初期化することになっているインスタンスプロパティを Optional にしない」に対するもうひとつの選択肢についても見ていけたらいいなと思っています。よろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #273
00:00 開始 00:27 今回の展望 01:26 lazy は確実な制御が難しくなりがち 02:46 lazy であれば self を使った初期化が可能 03:52 暗黙アンラップを制御する方法 04:35 lazy var は、参照時に mutating が必要 06:52 型安全を活かして制御する方法 09:57 型パラメーターで、出来ることを切り分ける 15:26 ここまでのおさらい 21:59 イニシャライザーでの型推論 24:07 準備機能のない型に振り替える 27:18 型によって nil な状態へのアクセスを防ぐ 29:18 メソッドチェーンで適切な操作を導くアイデア 33:10 立ちはだかる mutating 問題 38:55 幽霊型と呼ぶらしい 39:34 クロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #273
では始めましょう。今日も引き続き、Swiftのコードを書き換えてみましょう。このSwiftに関するブログがとても興味深くて、見ていると楽しんでひたすら追いかけている感じになっていますが、その中でも今日は前回お話しした内容の続きみたいな感じです。
後から必ず初期化することになっているインスタンスプロパティを安易にオプショナルにしないこと、必要がないのにオプショナルにしないことが提案されています。このブログではlazy
を紹介されていて、確かにそういった解消方法もありますが、lazy
にはいろんな副作用があって若干怖いところがあるよねという話を前回と前々回くらいでしました。制御が非常に難しい機能なので、なるべくならもっと別の方法がいいです。個人的におすすめなのは、暗黙的にアンラップされるオプショナルです。lazy
はあくまでも遅延評価の機能なので、後から必ず初期化する必要があるインスタンスプロパティとしては悪くない子もありますが、少し広すぎるんですよね。
そんな話をしつつ、他にも紹介した別の方法があります。例えば次のようなコードです。
struct Sample {
var value: Int {
return makeValue()
}
func makeValue() -> Int {
// 計算処理をここに書く
return 42
}
}
このように、遅延評価をしつつ、self
を使うことができます。また、上記のlazy
を使った例では次のようなコードになります。
struct Sample {
lazy var value: Int = self.makeValue()
func makeValue() -> Int {
// 計算処理をここに書く
return 42
}
}
この方法では、self
がまだ初期化されていない段階ではエラーになってしまうため、lazy
を使うことでself
を安全に使えるようにしています。インスタンスが初期化された後にのみ評価されるため、安全です。
そして、暗黙的にアンラップされたオプショナルを使う方法もあります。
struct Sample {
var value: Int!
init() {
self.value = self.makeValue()
}
func makeValue() -> Int {
// 計算処理をここに書く
return 42
}
}
この方法では、初期化後すぐにvalue
を設定するようにするため、アンラップエラーが発生しないようにしています。また、mutating
関数でプロパティを設定することも可能です。
struct Sample {
var value: Int
init() {
self.value = 0
}
mutating func initValue() {
self.value = makeValue()
}
func makeValue() -> Int {
// 計算処理をここに書く
return 42
}
}
このようにして、value
を適切に初期化してから使用するように制御することができます。これが現在のところお勧めの方法です。
let sample = Sample()
sample.initValue()
print(sample.value) // 42
このように、初期化後にinitValue
関数を呼び出してvalue
を設定することで、プロパティがしっかり初期化されます。
ここで、様々な方法を試してみましたが、自分に合った方法を選択して、コードをより効率的に、安全に保つように心がけてください。 さて、続けますが、ミュータブルという概念は非常に重要です。特に lazy var
にするとミュータブルでないといけません。どうしてかと言うと、初期化する時に値が設定されていない場合、それを書き込まないといけません。要は、self
の内容が変わるため、ミュータブルでないといけないのです。そのためには var
を使って値を書き込めるようにしないといけません。これもなかなか難しく、高層体で使うときには注意が必要です。クラスで使うときにはあまり気にしなくても良いですが、そのために lazy var
にしないといけないというのは結構大きな副作用です。ですので、あまりおすすめしがたいという話をしました。
今回お話するのは、前回の最後の方で少しだけ触れたコードです。ここに書いてありますが、型パラメータ(Generics)を使用して、プリペアが完了したかどうかを把握します。そして、プリペアが完了したときに限り、プロパティ X
を利用可能にするという仕組みの話です。今日はこれを解説していきたいと思います。この内容は少し難解ですが、理解できるように頑張ります。
まず、アプローチの大きなポイントは Y
というプロパティにあります。実装例として、Y
を Optional
にすることで、プリペアを呼ぶ前には値が設定されていない状態にします。ですので、プリペアを呼ぶ前に Y
を使いたくないという要望に応えることができます。こういった技術を紹介しようと思っています。
実際のコードはこのようになります。プリペアを呼ぶとプロパティ X
が使えるようになり、呼ばないと使えない、という仕組みです。使い方が少し難しいですが、コードのコンパイル時にこれを実現できます。
以下が基本的な例です:
class Example {
var value: Int?
func prepare() {
self.value = 10
}
func useValue() -> Int? {
return self.value
}
}
ここで、prepare
メソッドを呼ぶことで value
が設定され、それ以外では nil
のままです。これにより、プリペアが呼ばれる前と後で挙動が変わることがわかります。
型パラメータを使った例も見てみましょう。Generics を応用することで、型に対して型パラメータを添えて、その種類によって機能を分離します。具体的には、初期化される前にはイニシャライザーを提供し、初期化済みのときにはプロパティ X
を提供します。このようにして、初期化前は X
が無く、初期化後に X
が存在するという状況を作り出します。
struct Uninitialized {}
struct Initialized {}
class GenericExample<State> {
var value: Int?
init() where State == Uninitialized {
self.value = nil
}
init(value: Int) where State == Initialized {
self.value = value
}
func prepare() -> GenericExample<Initialized> {
return GenericExample<Initialized>(value: 10)
}
}
ここでは、GenericExample<Uninitialized>
と GenericExample<Initialized>
を使い分けることで、初期化状態に応じて異なる機能を持たせています。この結果、初期化前後で違う動作を実現することができます。
これで、ミュータブルと lazy var
を含む初期化についての説明は以上です。プログラムの状態管理をより柔軟にするためのテクニックを今回も学びました。引き続き、いろいろなパターンを試してみてください。 なので、ここではビフォーイニシャライザーが取れます。こんな感じで取れそうですね。すると、ビフォーイニシャライザーはさっきイニシャライザーと言ったけど、どこかで間違っていましたね。正確にはプリペア方法ですね。
ここでは、プリペアが両方に搭載されていることになります。プリペアが終わったらプリペアができている状態になるのですが、これを避けたいんですよね。どちらかというと2回プリペアをすると問題があります。一般的にそれを防ぐ方法としては、プリペアが終わったらプリペアを行わないように設定することです。プリペア後の状態をベースの定義には入れず、初期化の前だったときにプリペアができるように設定します。
こうすることで、まずビフォーイニシャライザーが取り除かれ、ビフォーイニシャライジングのプリペアが呼ばれることになります。このバリュー型(値型)がビフォーイニシャライジングになり、これはアタバアタバアンセーフビットキャスト(unsafeBitCast
)ですね。アンセーフダウンキャスト(unsafeDowncast
)の方がうまくいくかと思ったが、ダメでした。ちょっと厄介ですね。
プリペアがうまくできて、バリューのプリペアといった状態でこのバリューが得られるとします。こうなるともはやプリペアがなくなってx
が呼べる状態です。バリューの中に搭載されているのはx
プリペアですが、これはちょっとわかりにくいですね。
ビルドエラーになるのは、ビルドアタキャ(おそらく「アタバキャスト」)の仕方によります。例えば、こうして型パラメータを活用する方法があるわけです。これが関数型プログラミングでは結構メジャーな方法で、Haskellなどの言語で一般的に用いられているようです。
型パラメータを元にいろいろ判断するために、型パラメータを持ったジェネリック型を用意します。構造体に変えることもできますね。例えば、構造体に変えてみると動くかが確認できます。すでにダウンキャストを行うときには、コンビニエンス
(便利な)イニシャライザーが必要なくなります。
イミュータブルプロパティが設定できれば、これでプロパティの取得が可能になります。構造体の場合は、型がダイナミックに検査されているわけではないのですが、プロパティが表示されればOKです。クラスも構造体も、ジェネリック型でこのプロパティx
にアクセスできるのが初期化後にしたい場合には、内部にストレージ的な入れ物を用意する必要があります。
プロパティ初期化がプリペアを呼ばないと行われない都合で、強制アンラップのマークをつけてアンモカンラップされるようにします。これで初期化が実現されます。このエクスプロパティを初期化するためのイニシャライザーも持たないといけないので、self. _x
を初期化するようにしておけば、全体が機能します。
こんな感じで、ジェネリック型を使ったプロパティの構造と初期化を実現できます。 全部で共通の機能があればもちろん搭載してしまえばいいのですが、これボイドにしているのは Int
ですね。要らないかな、そうですね。それでビバ・イニシャライジングの中でいろいろ初期化するわけですから、プライベートなダミーのイニシャライズがいるんですね。
例えば、最初はここで初期化できないからアプリとして必要がないわけですが、本来は書くなら nil
を入れておくべきかもしれません。でも普通は nil
を入れておくというのが一般的ですね。
ここで Int
が必要ないから、この前ボイドにしたわけです。わざわざボイドとかにする理由は、ビバ・イニシャライジングの初期化で使いたいからです。今回はその都合でダミーのものを用意しないといけないんですね。
もう少し良い名前がいいですね。何かいい名前が思いつきませんね。どうしましょう。遅くなってもいいので、例えばアンダースコア x
で Int
の値をオプショナルで取ることにしておいて、とりあえず nil
で初期化してあげます。ちょっと無駄がありますが、このほうが別の意図を明示的にする形になります。
それで、この型パラメーターでビバ・イニシャライジングに対して規定のイニシャライザーを搭載します。これはプライベートではないので外からアクセスできる状況を作って、ユーザーにはこれを通してイニシャライズをしてもらう形にします。そうすることによって、全体で見たときにイニシャライザーが存在するということになります。
一般的には厳密に型を省略しなければ、ビバ・イニシャライジングにしかイニシャライザーが搭載されていないので、こういう形に書く必要があります。この既定イニシャライザーがどのジェネリックパラメーターでも有効ならば、ビバ・イニシャライジングにしか存在していないことが確認できます。
もし、ユニットみたいな形で他にも搭載されていると、曖昧性が生じます。この場合、ビバ・イニシャライジングの既定イニシャライザーを呼ぶか、Preparedの既定イニシャライザーを呼ぶかの選択が必要になります。しかし、唯一のデフォルトイニシャライザーがビバ・イニシャライジングにしか存在しない場合は、この曖昧性が生じないわけです。
この特性を利用して、Value型
のイニシャライズが可能になります。このようにして、例えば53行目の書き方が可能になります。普通の Value型
をイニシャライズしたのと同じ感じでコードが書けるようになります。
そしてここで Valueビバ・イニシャライジング
をPreparedで用意して、そこでいろいろ初期化します。例えば、以前の Xマータイ
を0に初期化しているのですが、普通はもっと複雑な初期化になると思います。こうして、型パラメーターに振り替えたい場合、一つの方法として、Preparedのイニシャライザーを呼んで初期化する方法があります。
もちろん、これでもいいですが、今回は全型パラメーター共通のイニシャライザーがあるので、それを使えば効率的です。このほうが賢いかもしれません。今回は明確にこうした方法が良さそうですね。 ただ、今回の場合はほとんどの型プロパティは型のボディ本体でしか定義できないので、ここにしか存在しません。どんな型パラメーターを設定したとしても、メモリレイアウトが構造体と変わらないので、これをそのままどんな型パラメーターだろうと、安全にメモリー配置を保てます。アンセーフビットキャストを使って振り返られるんですよ。メモリ配置が同じなので、prepared
もvalue
も初期化前に無条件に型変換だけをしてあげることができます。要するに、メソッドテーブルだけを入れ替えてあげる感じです。
どちらの方法でも構いません。個人的にはアンセーフビットキャストの方が好きですが、これは完全に個人の好みですね。上の方法のほうが冷静に考えると良さそうなので、そちらにしておきます。どうしますかね。この方法で良いでしょうか。
リターンを省略しようかとも思いましたが、どうでもいいでしょう。ただし、コードの見た目を考えると、prepared
の型にしてリターンを省略し、代入するほうが美しいですね。そうしましょう。ここでprepared
に変換し、初期化してあげると、prepared
でローカル変数にアクセスできるようになります。データは_x
なので、これを取ってきて使う感じです。
getter
とsetter
を使って、読み書きが可能ならこうやって設定してあげます。さらに、初期化が終わったらもうnil
が入る可能性がないので、ここはInt
型で良いわけです。nil
は不要です。prepared
なので絶対にもうnil
にはなりません。初期化が終わっているので、こういった感じで安全にアクセスできます。全面的に安全性を確保する方法です。
使用するときには非常に便利です。ただし、prepared
を忘れるとクラッシュする危険性があります。もしnil
が入っているとクラッシュするわけです。このような状況をソースコードレベルで防げるのがこの技法です。振り返る場合には、いくつかの方法を挟まないといけないです。
例えば、prepared
の段階では色々な処理が可能です。例えば、mutating func setFont
でフォントを設定したり、mutating func setColor
でカラーを設定したり、mutating func setLineHeight
でラインハイトを設定したりできます。全ての情報が決まっていないと操作できないような場合にも対応できます。ちょっと強引な例ですが、例えばマージンを設定するときには必要な情報が決まっていないとダメだとした場合、このように用意してあげます。
さらに、ファンクションでmakeValue
関数があり、これはValue
を返します。つまり、初期化が終わったものを必ず返すAPIにしてあげます。この中でバーを使い、バーが必要な処理をします。そして、リターンバリューとしようとすると、型が違うといったエラーが出ます。Before initializinning prepared
の型が違うということですね。
mutating func setFont(_ font: Font) {
// フォント設定の処理
}
mutating func setColor(_ color: Color) {
// カラー設定の処理
}
mutating func setLineHeight(_ lineHeight: CGFloat) {
// ラインハイト設定の処理
}
func makeValue() -> Value {
// 必ず初期化が終わったものを返す
return Value()
}
``` なので、これで `prepare` ってやればコンパイルが通るところですね。それでメソッドチェーンが可能になるんですよ。これが大事です。
関数的な変更をしないと活用できないので、例えばフォントを設定した後にバリューを返すようにしましょう。フォントを設定した後に `return self` かな。そうですね、これでバリューを返します。ここでバリュー、何も型パラメーターを指定していないのがわかりますか。ここでは特別なことではありませんが、何かと聞いてくると思います。
リターンセルフすることで、メソッドチェーンが可能になります。例えばフォントを設定して、次に別のプロパティをセットするという感じです。リターンセルフ後にメソッドチェーンが成立するので便利です。
また、これで `prepare` 前の状態ではフォントを初期化できて、`prepare` 後は初期化できなくなるような状態が実現できます。初期化が終わらないとできないことを防ぐと同時に、必ず初期化が終わったものを返すようにすることで、うっかり `prepare` されていないものをリターンすることを防ぎます。
例えば、カラーは初期化前でも初期化後でもカスタマイズできるようにする場合、このメソッドチェーンの技法が役立ちます。`ドットカラー.ブラック` などを使ってどちらの状態でも適用可能です。
SwiftUIのようなメソッドチェーンを駆使しながら初期化していくスタイルのものを作る際には、こういった技法をうまく使うといい感じです。ただし、`mutating` 関数などの制約に注意が必要です。
こういった技法を関数型界隈では `ファントムタイプ` と呼びます。由来としては、この型パラメーターはコンパイル時にのみ活用されて、ランタイムには持ち越さないという特徴があります。だから幽霊に例えて `ファントムタイプ` と呼ばれるようです。関数型と相性がいいので、興味があればこの辺りを掘り下げてみても面白いでしょう。
時間も来ましたので、今日はこれぐらいにしておきましょう。お疲れ様でした。ありがとうございました。