今回は、iOS 系のチップス集的なブログ 同じような処理だけどこっちの方がいいよってやつ の最後のところ、必須の遅延代入
を眺めていきますね。以前にも話題にのぼったテーマなのと、見どころもいろいろあってこれだけで今日は終わるかもしれないですけれど、これを見終わったらその次は再び本編に戻って ARC
の話題から クロージャーにおける循環参照
を復習していく予定でいますね。よろしくお願いします。
———————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #299
00:00 開始 00:12 ソフトエラーとハードエラーについての補足 02:48 エラー表現にみる工夫 03:36 専門用語は扱いに注意 06:02 Swift API デザインガイドラインのススメ 06:42 ゆめみオープンハンドブックで動画公開中 09:41 通常のオプショナルと、暗黙アンラップなオプショナルの違い 12:13 暗黙アンラップなオプショナルも適切な書き方 13:10 lazy 変数の使い所は、遅延初期化が必要かで判断 14:53 暗黙アンラップなオプショナルには nil を再代入しないのが良さそう 15:56 遅延初期化が必要なときに lazy を使う 17:35 著者の意図が書いてあるのは嬉しい 18:19 lazy var なら nil を代入できなくできる 19:33 lazy var 以外のアイデア 20:36 UIKit で @IBOutlet を使う場面 23:35 @ViewLoading による nil 回避 25:31 プロパティーラッパーを使って nil 再代入を防止する 32:51 クロージングと次回の展望 ————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #299
では、少し進めていきますが、まず前回の補足から始めます。この前、ハードエラーとソフトエラーの話が少し出てきたかと思います。自分が以前やっていた言語、たとえばMSX BASICの中で、確かにハードエラーとソフトエラーという言葉が出てきて、それでそれがどういうものかと思ったことがあります。
前回は、ソフトエラーが警告的で、ハードエラーが致命的なエラーというような話をしましたが、実際にはこの感覚は自分が勝手に身につけたものかもしれません。ネットで調べてみると、ハードエラーやソフトエラーに関する話が半導体とか電子機器の用語という感じでしか出てこないんですよ。もう少し調べれば出てくるかもしれませんが、ソフトエラーとハードエラーは、そうした半導体やメモリチップに関連するエラーのようです。そのため、ソフトに関わっている人たちは、あまり日常的に使う用語ではないと思います。
IT用語辞典にも、ソフトエラーは半導体チップ回りの話だという感じでしか載っていません。なので、前回お話ししたハードエラーとソフトエラーの概念は、私の中のローカルルールかもしれないので、注意してもらえればと思います。つまり、無視できるエラーと無視できないエラーのように捉えていました。
ただ、前回も話した通り、たとえばブーリアンで「これは無視しても良い」ということを知らせるのは良い方法の一つです。今表示中のエラーコードがある一定値以上だったら無視できない、という暗黙のルールを持たせるのも扱いやすいかもしれません。このようなやり方も全然ありです。
現在では、無視できるかどうかを示すプロパティを使って表現することができるので、その表現をちゃんと行えば問題なく使えるでしょう。ただし、そのときに例えばisHardError
のようにややこしい名前をつけず、もう少し分かりやすい名前にするのが良いでしょう。「リカバラブル」などの技術的な名前にして、アプリがリカバリできるかどうかを示すというイメージに持っていく方が望ましいです。
APIデザインガイドラインでは、まさにそのような名前付けが推奨されています。ハードエラーという言葉が本当に正しい意味で使われているのであれば問題ありませんが、もしハードエラーが自分の思い込みで使われているときには、半導体業界の人と意味が通じなくなる可能性があります。そのため、名前付けはなるべく一般的で分かりやすいものにしましょう。
例えばisリカバラブル
のようにして、反対の条件を表現するのがいいかもしれません。こうすることで、誤解を生むことなく、より良いコードが書けます。APIデザインガイドラインには多くの良いアドバイスが書かれていますので、一度読んでみることをお勧めします。
SwiftのAPIデザインガイドラインは非常にコンパクトにまとまっていて、Swift言語だけにとどまらない良い点がいっぱい書かれています。例えば、キャメルケースを使いましょうとか、そういうのは言語に依存する部分もありますが、名前付けに関しては今の時代に合った分かりやすいものにするのが重要です。
たとえば、あるプログラミング言語では8文字までという制約がある場合には、工夫が必要ですが、APIデザインガイドラインをぜひ読んでみてください。Swift APIデザインガイドラインに関する情報はネットで簡単に見つかりますので、英語が苦手な人でも日本語訳が出ていると思います。多少の訳の間違いはあるかもしれませんが、大きく意味が変わるような間違いは少ないでしょう。ぜひ参考にしてみてください。
この勉強会でもよく話題に上がりますし、公開されてもいますので、興味のある方は調べてみてください。APIデザインガイドラインについてもっと深く知りたい方は、Swiftの公式サイトなどを参照してみるのも良いでしょう。 こちらのページには、分野別に開催情報があり、その中に「APIデザインガイドライン」がありますね。これは第134回のものです。また、Googleドライブマークのものはまだ一般公開されておらず、社内の人限定となっています。ただし、YouTubeマークの方は一般の人も視聴可能です。第100何回といった回もちらっと見てみると良いかもしれません。このような基礎的な話題ですからね。それで、第22回以降や第12回以降を見ていくと、ある程度つかめると思います。APIデザインガイドラインを知っておくと、ずいぶん楽に書けるようになるのでおすすめです。
さて、ハードエラーの補足をしましょう。本編のブログタイトルに入った方がいいですね。同じような処理をする場合でも、こちらの方法の方が良いというブログがあります。今回の勉強会で取り上げる内容ですが、以下の必須の代入処理についてです。
var value: Int!
これは、調整アンラップのオプショナルという形になります。なぜ必須の遅延代入かというと、!
を使ってオプショナルを表現するとき、オプショナル型に対して暗黙アンラップのオプショナルを使ったというフラグが設定されるからです。このように動くわけですが、これを使うタイミングとしては初期化フェーズにあります。初期化フェーズの段階では値を確定できないけれど、実際に使う前の段階までは値を用意しておけるということです。
要するに、変数を扱うときには初期化フェーズ、代入フェーズ、参照フェーズという3つのフェーズがあります。これがソフトウェア工学の基本です。初期化フェーズのときに初期化ができない事情があるときもありますが、初期化から参照フェーズに入る前までには必ず代入できる、そんなときに暗黙アンラップのオプショナルを使います。つまり、初期化フェーズから遅延して代入するのです。ここには書いていませんが、参照するときには値が決まっているという事情です。
参照フェーズになっても値が入っていない可能性がある場合には、普通のオプショナル型を使います。これがソフトウェア標準の考え方です。この標準の方針を理解しておくと、次のステップでこの書き方よりも良い方法が見えてくるかと思います。
この勉強会に参加している方々は、ここから何が出てくるのかを予想することでしょう。個人的には、この書き方自体もそんなに悪くないと感じています。ですから、これは悪い書き方だと思わずに次を見ていただきたいという希望があります。その点を覚えておいてもらえると嬉しいです。 この書き方、悪くないですよ。書き方が悪いと感じるなら、それはむしろこの書き方が悪いということになるかもしれません。しかし、一つの書き方としてこういった方法もあるということで了解しておきましょう。
言語使用的には、若干オーバースペックな用語やキーワードを使っている感はありますが、確かに可能です。例としては、これは遅延初期化というキーワードになりますね。ちょうどここが該当します。遅延代入という観点で見たときには、悪くないですね。その遅延代入を使うべきかどうかをしっかり見極めて、問題ないと判断したらアリな書き方と言えます。
上の方もそうなんですが、下の方法がより適用範囲が広くなりすぎてしまうのが問題です。遅延代入と言っても、バリューは Int
型で遅れて初期化される、その初期化式はこれだと言っているだけです。言語使用としては、参照時までには必ず値が入っているという意味合いのもので、逆にそこを捉え間違うと変な所で使い始めてしまいます。そのため、この辺はどちらを使うべきかの判断を大事にしてほしいですね。
参照フェーズになれば既に値が入っていることを約束するための書き方なので、一般的にビックリマークを使ったオプショナルの時に代入演算で nil
を入れるということはありえないと考えておいた方がいいです。そうしておいた方が適切な使い方ができます。もし使うときに nil
を入れるような状況があったら、素直に ?
に変えてみてください。どちらを使うか迷ったら、今お話した内容を参考にしてください。
例を使う場合ですが、「ファン」として支援機関として用意してもらい、使うことができます。このように、代入されない限り preconditionFailure
で落とすという使い方をします。こうすることによって、 Int!
のときよりもプログラマーの意図がより反映されます。こう書くことにより、上記の方が汎用的で、下記の方がより特化された感じになります。
どっちを使うかとなったときに、このコードを見て、慣れていればわかりますし、慣れていない場合でも preconditionFailure
になっているとわかります。どちらを使うかはその状況に応じて判断してください。必ずしも下の方法が良いとは限りませんが、使ってみて欲しいですね。このブログの趣旨と反して強調しますが、ここでは著者の意図が書かれていて良いですね。こう書いてあると、なぜそちらに書き換えるのかがわかるので、やはりブログでは自分の意見を書くほうが良いです。
遅延にすることで代入前に呼ばれると前提条件例外である preconditionFailure
が叩かれるようになります。これは増強化の前提条件として preconditionFailure
が叩かれるようにコードを書いています。ここは大事です。 nil
を再代入できない状態にできるのは大きいですね。確かに、先ほど言った通り、こっちは nil
代入できちゃうから「しないでくださいね」と注意しないといけないですよね。プログラマのレベルによっては実際に nil
を代入するかもしれないので、その点で解釈にはレベルが必要ですが、読むときにはレベル不問で、いや応なく nil
代入できなくするのは良いですね。
こんな感じで、ブログの読み終わった後の感想です。気づいた点を追記するので、何かあればコメントくださいと言っています。コメントを見たときに寄せられていなかった場合は、自分で書き加えるのも良い方法ですね。
このブログの内容はここでおしまいです。次に、レイジー初期化に関する別のアイデアについて紹介します。勉強会で既に出てきた方法の一つですが、今のブログで見たものだったかどうかはちょっと曖昧です。
勉強会で出てきたのが @lazy
という手法ですね。これはとても便利です。例えば UIKit
の UIViewController
において、特定の変数を lazy
で初期化することがよくあります。UIViewController
を使用する際には、通常 awakeFromNib
や viewDidLoad
が呼ばれた後に初期化され、その中で何かしらのバリューを使いたい場合です。
UIViewController
のライフサイクルによって初期化が保証されるので、強制アンラップのオプショナルを使うことが可能です。たとえば、UIラベルをインターフェースビルダーで接続する際には、実際に Storyboard
から連結されて使える状態になっています。この場合、強制アンラップのオプショナルである !
を使う書き方が一般的です。これは初期化フェーズでは決まらないけれど、ライフサイクル的に viewDidLoad
や awakeFromNib
の時点で初期化されていることが保証されているためです。
これはライフサイクルを尊重していれば問題が起こらないはずです。個人的には、問題が起こらない作り方を積極的に進めてもらいたいと思いますが、この方法が最も妥当だと思います。@IBOutlets
を使って初期化が完了しているかを確認する方法もあります。
以下は @IBOutlet
の例です:
@IBOutlet weak var myLabel: UILabel!
この書き方により、UIコンポーネントの初期化を簡潔に行うことができますし、初期化されていない状態でアクセスすることを避けることができます。このように適切に初期化の保証がされている場合、強制アンラップのオプショナルを使う方法がベストです。
次回は、さらに別の初期化に関するアイデアを紹介していきます。質問やコメントがあれば気軽に教えてくださいね。 とりあえず初期化フェーズは何もしないでパスしますが、実際にはこれはアプローチが逆です。準備がされて終わった後に参照するという話ではなく、こちらの場合は逆に参照のタイミングになって準備ができていなければ準備をする、という動きを見せます。ユーザー側から見ると参照したときに準備をしてくれるという流れですが、広く見るとユーザープログラマー側から見ると参照フェーズでは必ず初期化が終わっているということが確約できるわけです。
このビューローディングを使うというのは一つの選択としてありです。例えば、UILabel
がオプショナルではない場合、どう頑張ってもnilが入らないようにできます。これはスマートなやり方で、UIKitの世界ではとても良い案です。ただし、これには欠点というか適用できない場面があって、適用できるのは型が参照型に限られています。そのため、UIView
しか使えない状況が課されるのです。ですから、この方法は良いのですが、使用場面が限定されてしまいます。
他のアイデアとしては、同じ発想でプロパティラッパーを使う方法があります。例えば、Int
のプロパティを初期化フェーズで初期化できない場合、通常はInt!
(インパリティブリマーク)を使いますが、個人的には良いと思いますが、nilが代入される危険性を排除したい場合があります。そのような時に、lazy
を使うと少し意味が広すぎてしまいます。そのため、より意味が狭い方法としてプロパティラッパーを使うのが良いです。
ここで一つプロパティラッパーの例を書いてみましょう。以下は、遅延初期化を行うプロパティラッパーのコードです。
@propertyWrapper
struct LazyInitialization<T> {
private var value: T?
private let initializer: () -> T
init(wrappedValue: @autoclosure @escaping () -> T) {
self.initializer = wrappedValue
}
var wrappedValue: T {
mutating get {
if value == nil {
value = initializer()
}
return value!
}
set {
value = newValue
}
}
}
遅延初期化を行うには、このプロパティラッパーを利用します。使用例は以下のようになります。
class Example {
@LazyInitialization
var number: Int = 42
}
これにより、number
プロパティは初めて参照されたときに初期化が行われます。この方法で、意味を広げずに必要なタイミングでのみ初期化を行うことができます。 とりあえず定義しておくと、初期値を代入できるようになります。しかし、レイジイニシャライゼーション(遅延初期化)を使いながら初期値を代入させるのは、ちょっとオーバースペックなインターフェースになってしまいますね。これは不要だと思います。
それで、大体完璧です。もし、この中身にアクセスさせたい事情があって、そのようなインターフェースを提供する必要がある場合は、projectedValue
を使って、自分自身の型を返す形にします。これで、例えばトレットオブジェクト
のオブジェクトを$value
の形で呼べるようになりますね。
少し早めに説明しておくべきでしたが、このvalue
はインターバルなので、オプショナルを気にせずに呼び出せます。現在は初期化していないので呼び出すとエラーが出ます。しかし、別に$value
だとレイジイニシャライゼーション型が取れるので、先ほどインターナルのままにしておいたvalue
も呼べるようになります。このようにすると、よりローレベルな方法で扱えるようになります。「ローレベル」ということで、例えばlowValue
と名付けたほうが分かりやすいかもしれません。このようにして、生の値をアクセスできるインターフェースも、必要なら提供できます。
ただし、そうするべきではないことが多いので、基本的には避けたほうが良いですね。代入したコードはpoweredBy
にしないとダメかもしれません。これはなんだ、コンテキストエラーですね。読みづらいですが、「コンテキストエラータイプ」という表示です。これはおそらくInt
型だと思うんですけど、代入できません。
現在のコードには、value
というプロパティがないため、lowValue
に対して代入ができないようです。この部分の構造が問題なのでしょう。ただし、オブジェクトプラス
だったので、代入できるはずなんですが、何か見落としているかもしれません。セッターが必要だとは思わなかったので、セッターを用意すれば大丈夫かもしれません。では試してみましょう。
セッターを追加して、lazy
イニシャライゼーション型を使う場合も注意が必要ですね。代入インターフェースをあまり用意できないので、代入させたい場合には単純な型で持たせるというのが賢明かもしれません。ダイナミックセルファーやファイナルクラスにしないといけないとか、それに基づくイニシャライザーの問題かもしれません。
以上で、今日の内容はおおむねこんな感じでしたね。次回は、オートマチックリファレンスカウンティング(ARC)やクロージャーの循環参照解消の話題に戻ります。これは1ヶ月、いや2ヶ月前の話ですね。そこに戻っていこうと思います。
それでは、これで終了します。お疲れ様でした。