https://youtu.be/CAcDWW0Yndw
今回は オプショナル
の主要な基本機能 オプショナル バンディング
について見ていきます。誰もが親しんでいると言っても過言ではなさそうな機能で、これまでにも何かのついでに幾度と眺めてきましたけれど、The Swift Programming Language の Tha Basics ではどんな視点で扱っているのかみたいな気持ちで改めておさらいしていって見ますね。よろしくお願いします。
⋯ と、オプショナルバインディングを話そうと予告しておいたものの、今になって強制アンラップについてもう少し話したくなってきたのでそちらにするかもしれません ←
—————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #151
00:00 開始 00:08 今回の展望 00:37 unsafelyUnwrapped の挙動を確認 03:17 強制アンラップと -Ounchecked 04:34 unsafelyUnwrapped は使わなくても良いかもしれねい 05:30 チェックしなかったときの値は? 11:47 Any がゼロフィルされてトラップする可能性 15:27 unsafelyUnwapped の実装を眺める 16:46 _unsafelyUnwrappedUnchecked 関数は使える? 19:46 unsafelyUnwrapped が使われている場面 23:23 nil な Data を unsafelyUnwrapped で取得すると? 24:03 nil な列挙型を unsafelyUnwrapped で取得すると? 24:33 名前修飾 26:52 unsafelyUnwrapped に起因する実行時エラー 30:16 NonEmpty ライブラリーから考える unsafelyUnwrapped の利用場面 32:02 クロージング ——————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #151
前回の内容について少し画面外で映っていますが、今日はオプショナルバインディングの話をしようと思っていました。ただ、強制アンラップの方に興味のある内容が出てきたので、そちらを先に話そうと思います。勉強会は週3回開催されているので、ゆっくりと強制アンラップについて見ていきます。
前回の最後に教えてもらった「アンチェックフリーアンラップ」についてですが、確認してみたらとても面白いことが分かりました。実際にリリースビルドで無視する動きになっていて、これには驚きました。ですので、まずはこの観察結果を皆さんに共有したいと思います。
たとえば、Playgroundで確認するのが一番簡単ですが、コンソールで以下のように試してみます。
var value: Int? = nil
print(value!)
これを swiftc
でコンパイルしてみます。例えば、オプティマイズなしで
swiftc -O0 sample.swift
./sample
このようにやると、アンチェックフリーアンラップで強制終了します。しかし、リリースビルドのときに
swiftc -O sample.swift
./sample
この場合も動作します。これは本当にすごいことで、オプティマイズがかかったときの動作の違いが見て取れます。
一方、強制アンラップとアンチェックフリーアンラップの動きは違います。前回の最後にお話ししたとおり、リリースビルドでの強制アンラップはトラップがかかり、エラーで終了します。たとえば以下のコードを最適化して実行すると、強制アンラップの部分でトラップがかかり終了するはずです。
var value: Int? = nil
print(value!)
swiftc -O sample.swift
./sample
一方で、アンチェックフリーアンラップと同じように動作させることもできます。
var value: Int? = nil
print(value as! Int) // これはエラー
これを最適化すると、動作するようになります。
このような最適化によりパフォーマンスが向上することがありますが、一方で注意が必要です。普段のリリースビルド時では、強制アンラップの適切な場所にトラップがかかってエラーで終了します。しかし、アンチェックフリーを使うことでそれを無視して実行することができるので、内容を適切に理解し、使用には慎重を期すべきです。
これらの動作はストリング型など他の型でも確認できます。例えば、以下のコードで動作を確認してみます。
var value: String? = nil
print(value!)
この場合も、アンチェックフリーアンラップを行うことで空文字になることがわかりました。これは意図的に空文字にしているのか、たまたま空文字になっているのかの違いがありますが、いろいろな型で試してみるのも面白いです。
さらに以下のようなコードでタプルを用いて確認してみます。
var value: (Int, String, [Int])? = nil
print(value!)
アンチェックフリービルドをかけると、これらの型もゼロなどの初期化値になることが確認できました。しかし、これが実行時のパフォーマンスにどう影響するかも気になるところです。
最終的には、中間言語の出力を確認してみる方法が考えられます。例えば、以下のコマンドで中間言語を出力して確認してみます。
swiftc -emit-ir sample.swift
このようにして、中間言語の詳細を確認することもできます。これにより、最適化がどのようにコードに影響を与えるかをより深く理解することができます。
まとめると、パフォーマンス重視でない場合はリリースビルド時のアンラップで十分です。しかし、パフォーマンスを重視する場合には、アンチェックフリーを使う選択肢もありますが、慎重に使用するべきです。 とりあえずここまで見てきましたが、オプショナルの Int
や String
の初期化について話していますね。ここで重要なのは、オプショナルに nil
を入れて初期化することです。この場合、オプショナルの nil
はゼロと同等に扱われることがあるという点について触れていました。これは以前の勉強会でも説明したところです。
次に、アンチェックアンラップ(無効なアンラップ)のトラップについての話に移りました。例えばオプショナルの Int
を利用して初期化する場合、特定の条件でトラップが発生することがあります。サンプルコードを使ってその動作を確認するための実験を行っているようです。
let optionalInt: Int? = nil
print(optionalInt) // nil
この nil
の初期化が原因でゼロが埋め込まれているかどうかの検証中ですね。例えば、オプショナルを nil
に設定した場合に実際にはゼロフィルされているかの確認です。この辺りはメモリレイアウトの詳細によるものなので、実際に動かして確認する作業が必要になります。
続いて、 Any
型のオプショナルについての話に移っていきました。 Any
型の場合、内部で独自の構造を持っているため、どんな型でもラップできるという特殊な特徴があります。これにより、メモリの解釈時に特定の条件でエラーが発生することがあります。
let anyValue: Any? = nil
print(anyValue) // nil
上記のコードで Any
型がゼロフィルされるかどうかも確認していました。さらに、 Any
型のメモリサイズがどれだけかについて議論しました。
質問やコメントをまとめると、標準ライブラリーに @_unsafeUnwrap
というアンラッピング用の関数があることを紹介していました。この関数はデバッグビルドで使用できるもので、特定の条件下でのアンラップエラーをトラップするために使われます。
これで、大体の流れが捕捉できました。全体として、オプショナルや Any
型の動作、特に nil
の取り扱いやメモリレイアウトについての詳細な検証をしている内容が中心でした。 標準ライブラリのアンダースコアで始まっている関数について話してみましょう。これらの関数は内側で使うためのもので、手動で調整するためのものなのでしょうね。デバッグビルドだとしても問題ないか、パスするようになっているのだと思います。これを表で使えるかどうか試してみます。
次に、頭のドットが何を意味するのかについてですが、これはドットを書いていますね。ドットが書いてあるということは、特定のコンテキスト内でのメソッドを意味しています。所属しているものとフリーな関数という感じですね。インターナルインバリアントというのもフリーなインバリアントの一つかもしれません。
インバリアントというのは、普遍という意味です。内部で使われるインバリアントのことを指しているのかもしれませんが、具体的な使い方についてはもっと詳しくないと理解が難しいですね。
次に、このアンセーフ・リアンラップ・アンチェックという関数を試してみましょう。以下のようなコードを書いてみました。
let value: Int? = nil
let unwrappedValue = _unsafeUnwrap(value)
これを実行すると、関数が見つからないというエラーが出ますね。エラーメッセージによると、内部的なプロテクションレベルが設定されているようです。これを試すためには、標準ライブラリを直接扱う必要があるかもしれません。
何かインポートする必要があるかもしれません。例えば、ビルトインの変数を使うときにはimport CompilerBuiltins
を使うと良いと教えてもらった気がします。さらに、コンパイラーオプションとして-parse-stdlib
を使う必要がありそうです。
最後に、アンセーフ・リアンラップ・アンチェックがどこで使われているのかについて調べてみると、first
のような場合で使われていることが分かりました。確かに、配列の最初の要素が必ず存在すると分かっている場面では便利です。 そういったところで、「アンセーフアンラップ」を使うと確かに早くアクセスできるんです。どういう状況かちょっと考えるとなかなか思い浮かばないですけど、ありますよね。例えば、配列があって最初の要素を使うときに、first
ってオプショナル型だから、普通は!
を使いますよね。値を変数にして、その変数から取ると分かりやすいかな。こんな感じです。
通常、ここでビックリマーク(!
)を使いますが、「これは絶対に存在するよね」って強調したいときに、アンセーフアンラップを使うわけです。確かに、自信があるときにはアンセーフアンラップを使ったほうがいいかもしれませんね。
ただし、他の人から見たら危険だと感じるかもしれません。Lint(コードスタイルチェックツール)の中には、!
の使用を禁止することが多いので、それを避けるためにアンセーフアンラップを使うのはいいなと思いました。実際、ビックリマークを使っていると、もしその値が存在しなかったらクラッシュしてしまいますし、他の方法でアンラップしても結局は同じように一定の状態である必要があります。
具体的に言うと、デバッグ中はクラッシュするけど、リリースビルドではクラッシュしない設定にすることもできます。デバッグ仕様ではクラッシュして、リリースビルドではゼロや空文字になるような違いが出るということですね。この値が必要な場合にどうなるかというと、ゼロや空文字になることが多いです。
データがゼロバイトになるのは少し怖い感じもしますけど、事実としてそう処理されます。列挙型の場合、先頭の要素が選ばれるなどの規則があるので、例えば列挙型 EnumType { case A, B, C }
なら、最初の A
が選ばれることになります。
他にも、"マングリング"と呼ばれる名前の配置があり、関数を呼ぶときにどのアドレスにあるかを管理する方法です。C言語などで使われ、メソッドシグネチャ(関数の型や名前を含む署名)で管理します。これがマングリングです。マングリングとデマングリングという言葉があるので、後で詳しく調べてみたいと思います。
途中で引っかかっているのは、名前のマングリングです。テキストが中途半端で誤った表記が含まれているようですが、とりあえずそのつもりで進めますね。 名前の中でシンボルを使えない場合、混乱することがありますね。次に、EmptyTable
およびnil
関連のエラーについてですが、エラーが発生していましたね。時代を踏んだ感があるかもしれませんが、これは一度配列として試してみると何か分かるかもしれません。
送ってもらったコードはTypeDecoder
とSwiftコンパイラ関連ですね。エラーの概要を調べていただいたようです。ゼロフィル(zero fill)をやってみても、たまたま実質的にそうなっただけかもしれないと感じました。
まずはゼロフィルを試してみましょうか。今のはランタイムエラーなのかどうかですね。まずはコンパイラでのエラーを確認してみましょう。コンパイルが通るなら、ランタイムエラーを確認する形です。
例えば、print(1)
を実行すると、print(1)
の後にエラーが発生します。それで、これがランタイム警告だと分かりました。警告が出ても、プリント文が表示されないことがありますね。Foundation
をインポートしてみると、状況が変わるかもしれません。
再度、Foundation
をインポートしてみます。一度インポートした後で、またエラーが出ました。値を取り出すかどうかが問題のようです。値が壊れている場合、ダンプ(値を表示する)してみると状況が分かるかもしれません。例えば、エッジケースが出てくるかどうか試してみると良いでしょう。
アンセーフアンラップ(!
)の使用例ですが、ライブラリで必ず空でない配列を提供する場合に便利です。このように型システムで保証されている状況なら、アンセーフアンラップを使っても安心です。
例えば、APIの設計者が責任を持ってアンセーフアンラップを利用するケースですね。APIの外部で使う場合には注意が必要ですが、内部であれば問題ない場合もあります。
今日はこの辺で終了とします。お疲れ様でした。ありがとうございました。