Page icon

第141回

2022/07/27
番外編
Empty
Empty
Empty
Empty
Empty
13 more properties
今は 
オプショナル
 について眺めていっているところですけれど、その特殊性からついついいろいろ脱線気味です。今回も、前回の最後の方に教えてもらった参照型を扱う際の 
オプショナル
 にみられるメモリー確保の様子が面白かったのでもう少しだけ詳しくそれを見てから、改めて 
オプショナル
 の基本に戻っていこうと思います。よろしくお願いしますね。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #141
00:00 開始 00:10 今回の展望 01:38 多重オプショナルのメモリーサイズ 03:44 前回の自分の認識は間違い 04:59 それぞれの段数に応じた nil を示すポインター 13:17 オプショナル型は最大で何段階まで多重化できるか 14:09 オプショナルの段数の範囲はなぜ 9 ビットなの? 16:47 オプショナル型を限界まで多段にしてみる 17:27 型パラメーターの段数上限と、実の型としての段数上限 22:15 自作の nil が機能しているか確かめる 25:35 オプショナル型にインスタンスが入る場合も自作してみる 26:02 あくまでも参照型を扱うオプショナル型での話 26:49 こうしてオプショナル型を見てきた印象 27:14 NULL ポインターと 0 番地 28:35 メモリー保護 28:50 クロージング ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #141
引き続き、オプショナルの話をしていこうと思います。前回、オプショナルが何重にもなったときのメモリの取り方に関する話題がありました。これが非常に面白かったのと、私自身の認識が間違っていた部分もあったので、もう少し詳しく見てみたいなと思います。
この話はアプリケーションを作る上では直接的には役に立たないことが多いですが、機会がないとなかなか見ることがない部分ですし、詳しい方々が関わる話題でもあるので、こうした話を聞くことでステップアップの機会につながるかもしれません。では、そのあたりを再度確認していきましょう。
具体的に、オプショナルが何重にもラップされたときの話です。例えば、ストラクトがあって内部に
Int
型の値を持っているとします。ストラクトが 8 バイトのサイズだとすると、このストラクトがオプショナルになると 9 バイトになり、さらにオプショナルが重なると 10 バイトになります。
しかし、このストラクトがクラスになると状況は変わります。値型から参照型に変わると、8 バイトのサイズが同じ状態で続くのです。この違いは前回教えてもらって、とても興味深い点でした。また、クラスの場合、イニシャライザを必ず実行しなければならないという点で異なりますが、今回はストラクトでもう一度確認してみましょう。
例えば
Optional
を 3 重にした
MyValue
型があったとして、初期化されているとします。このときのメモリの取り方について、以前は
MyValue
がポインタで、各ポインタが 8 バイトを指していると考えていました。しかし、この認識は間違いだったようです。実際のコードを見ると、全く異なる最適化が行われています。
具体的には、ポインタが指しているアドレスが
nil
の場合、ゼロポインタを指しています。これが特定の階層の
nil
の意味を持つようになっています。これを確認するために、複雑なコードが必要かもしれませんが、例えば
bitcast
を使ってゼロを試してみると、初期化されていないことが確認できます。
ストラクトと参照型のオプショナルの違いを見てみましょう。例えば、あるインスタンスがあり、その中に 8 バイトのメモリ領域を持っているとします。これがオプショナルで包まれている場合、そのアドレスが
nil
の場合にはゼロになります。しかし、アドレスが1や2、3と異なる場合でも
nil
として認識されます。これが4になると
bad access
になるわけです。
このように、オプショナルの階層が増えるたびにメモリの取り方が異なり、特定の値が
nil
として扱われることがわかります。普段はあまり意識しない部分ですが、詳しく見ていくと思わぬ発見があるかもしれませんね。 バックアクセスについてですが、ここでレッドオブジェクトを
let obj = myValue
としてインスタンスを作成します。そうすると、このアドレスの部分に実際のポインターを渡すためにはどうすれば良いのでしょうか。これは
let objAddress = UnsafePointer(&myValue)
のようにして、バリューを渡すことができます。
戻り値として受け渡されたアドレスを取得し、このアドレスを渡せば良いのかと思います。ここで
UInt
に変換しないといけないのかもしれません。結局のところ、アンマネージドな(管理されていない)アクセスを使用した方が楽かもしれません。とりあえずイメージ通り動くか試してみましょう。これもバックアクセスとして動作するようです。
やはり、関数風の場合、この辺りの無理やりな方法は仕方がないですね。アンマネージドパス、アンリーティングなオブジェクトとして
let unmanagedObj = Unmanaged.passUnretained(myValue).toOpaque()
を使うとどうでしょうか。オブジェクトのアドレスをそのままこのポインターのところに渡して、ビットパターンを壊さずに
UInt
にキャストし、それをアドレスとして渡すと、ちゃんと
myValue
が出ているのです。
この辺りは、本当にこのオブジェクトで作った
myValue
なのかを確認するために、エクステンションで
MyValue
CustomStringConvertible
にし、
description
としてバリューのディスクリプションを返すようにしましょう。それで実行するとゼロになりますよね。例えばオブジェクトのバリューを100にすれば、100になるわけです。
こういうふうにポインターをそのまま渡して、
myValue
にキャストすると、ちゃんとオブジェクトのインスタンスが存在することが証明されます。ここで、自分が間違っていた話が明らかになったと思います。
このようにリンクされたポインターではなく、完全に
myValue
がもう一度参照されている状態です。さらに、参照型のオプショナルについてもこうした最適化がされています。例えば、オブジェクトが
nil
であれば、それは
nil
ですし、1だったら1、3だったら3というように、段階を踏んで、ソフトな参照とダイレクトな参照が最適化されています。
構造体の場合は、1ビット多くフラグを持っていて、それがオプショナルかどうかの情報を付与して管理します。これが参照型と値型の違いです。このようにメモリ配置が異なりますが、それぞれに適した方法で最適化が図られているのです。 なかなか自分の中では特に「ヌルポインター」という言葉はよく聞きますが、「1ポインター」という言葉は聞いたことがありません。しかし、「2ポインター」や「3ポインター」など、いろいろとポインターのアドレスを応用してまとめてくれているようです。
前回の勉強会で、中作何回かという話題でオプショナルのラップ回数が何重までできるかという話がありました。そのときのスラックでの情報共有によると、オプショナルのラップ数の制限が大体255だったと思います。とにかく制限が厳しいのは、オプショナルの扱いがポインターと関係しているからかもしれません。
このポインターに関して、512という数値がしばしば出てきますが、どうして512ビットが上限なのかはよくわかっていません。もしかして、何か特殊な内部の都合があるのかもしれません。例えば、特別な値が指定されているとか、コンパイラの設計思想が関係しているとかが考えられます。具体的には、
0x200
が512という意味で、16進数だと512になります。しかし、それがなぜかはまだ解明されていません。
実際にコードを試してみて
512
が上限だとわかったのですが、さらに調べていくうちに
513
になるとポインターとして有効に働かないことがわかりました。繰り返し実験しても同じ結果でした。ここで一つ気をつけなければいけないのは、メモリレイアウトの問題や環境による違いが影響しているかもしれない点です。
いずれにしても、典型的なメモリサイズの問題や、処理がうまく動くかどうかは試行錯誤が必要です。たとえば、コード内でいくつまで正しく動作するかを確認するためには、一定の手間がかかりますね。実際に試してみると
511
までは動くが
512
を超えると動かないなど、具体的な数値を確認することができます。
総括すると、オプショナルのラップ回数やポインターの上限など、まだまだ解消されていない謎が多いですが、こうした検証作業を通して次第に理解が深まるのではないかと思います。もし有効な理論や推論があるなら、ぜひ共有して議論したいところです。 さて、現在試しているのは511ですけど、ちょっと曖昧な感じで確信が持てません。でも、どうにもおかしいですね。実際には、510でなければnilになるはずです。512をコピーしてきたと思いますが、間違いでした。512がエラーになるのは確認しました。こういった試行錯誤を繰り返すうちに混乱してきます。
エラーが発生する理由として、例えば「はてなが1個だと0」という現象があります。つまり、実際の数より1個少ない状態になるということです。ですから、はてなが511個なら510までしか認識されません。これでnilとなり、最上位のオプションとしてもnilという結果になります。コメントでの指摘によると、やはり511みたいですね。そうなると、8ビットではなく9ビットということです。512にすると1個エラーになるので、実際には0から511の範囲になります。ここでは、ビットの利用効率が良好ですが、9ビットの問題は解消できません。
このような状況ですが、Swiftのオプショナルの機能について話しましょう。アドレスをゼロとして設定した場合にも、何のエラーも発生しないことがありますが、その理由がわかりません。その際、「サイズが違う」と言われることもあります。ただし、動作上の問題はありません。nilの判定も通常通り行えます。
例えば、
if let y = optionalValue { } else { }
のような判定が普通にできるということです。特定の条件下でnilになるかどうかという判定も行えます。ここで重要なのは、オプショナルの階層構造です。1段階のオプショナルと2段階のオプショナルでは異なる動作をします。
switch optionalValue { case .none: print("Nil case") case .some(let value): print("Value: \\(value)") }
このようにスイッチケースを使うと分かりやすいです。例えば、オプショナルの中にオプショナルが入った場合、条件によってはnilではないこともあります。
また、アドレスの確認には
UInt(bitPattern: address)
を使うと、
Y
が得られます。これにより、カスタムストリングコンバーティブルが得られ、その結果として期待通りに動作することが確認できます。
このように、Swiftのオプショナルについてしっかり理解することが重要です。 面白いですよね。これが単純型のオプショナルの話です。これがストラップ(配列)になってしまうと、そもそものバイトタイプが変わってくるから、どこで得られるかと言うと、そもそものメモリマネージメントが関わってきます。これは当たり前ですが、ビットテストのサイズが異なってくるんです。たとえば、ここが8バイト、9バイト、10バイト、11バイトになっているので、データタグの中身は8バイトなのですが、サイズがズレてきてしまうという問題があります。こういった問題は単純型に限定されますが、このような特徴が見えるわけです。
今、ちょうどいい具合の時間になってきましたので、そろそろ勉強会を終わりにしようかと思います。こんな話が役に立ったかどうかはわかりませんが、自分としてはとても面白かったです。特に見落としていた NullPointer の部分が面白かったですね。
NullPointer については、単にポインターのアドレスがゼロになっているだけだと思っていたのですが、調べてみると面白いブログを見つけました。それは、C言語でゼロ番地にアクセスする方法についてのまとめです。これを見て衝撃を受けたのが、ゼロポインターはコンパイラーが特別扱いするということです。処理系でコンパイラーがゼロだった場合、最適化の状況によって特別にエラーにするのです。メモリプロテクションでエラーになるのだと思っていたのですが、この点が見落としていたところでした。
NullPointerや
1
2
といった特定の値を意識することで、より丁寧な理解が深まりそうだなと思いました。今回、オプショナルに関するお話もいろいろさせていただきました。
コメントとして「メモリプロテクションは仮想メモリ機能がないと動かない」といただきましたが、確かにその通りですね。だからコンパイラーの方で安全性を万全に期すのでしょうね。
以上がメモリ周りのお話でした。これで勉強会を今日は終わりにします。お疲れ様でした。ありがとうございました。
今回は 
The Basics
 の 
オプショナル
 について、その具体的な特徴について引き続き眺めていきます。これまでのオプショナルそのものに焦点を絞った感じよりかは 
nil
 や初期化周りの特徴といった、少しその外回りに視野を広げていくような感じになりそうです。よろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #142
00:00 開始 00:10 今回の展望 01:17 nil はリテラル 01:47 リテラルがインスタンス化される仕組み 05:21 nil リテラルがインスタンス化される仕組み 06:10 ExpressibleByNilLiteral 07:25 整数リテラルと nil リテラルとで異なる挙動 08:50 総称型とリテラル変換 09:53 型推論で型パラメーターを特定 11:37 配列の内容を nil で初期化するとき 14:21 できないと思い込むこと、わりとよくある 15:22 型推論を使った別の書き方 17:02 Double?.none という書き方 18:12 自身の型を推論させる書き方 18:36 表現のバリエーションを増やしておく 19:31 reduce を用いて表現してみる 20:56 シーケンスと zip や prefix で表現してみる 24:06 AnySequence で乱数発生器 25:32 コンパイル時に適切な値を決め打つ方法 26:41 クロージング —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #142
はい、じゃあ始めていきますね。今日はオプショナルについてお話しします。オプショナルについては以前からよく話題にしていますが、今回はその機能面の基礎をもう少し言語寄りに広げて見ていこうと思います。
まず、画面に映っている「nil」という存在についてです。これまでも何度も出てきましたし、Swiftでコードを書く際には日常のことかと思います。Swift Programming Languageにも「nil」というセクションやサブセクションがあります。今回、この「nil」というものにスポットライトを当て、それがどのような世界を見せてくれるのか、一緒に見ていこうと思います。
「nil」ですが、オプショナルな変数に特殊な値「nil」を割り当てることで、その変数を値のない状態にすることができます。この「nil」というのは値というよりリテラルに近い存在です。ここで少し脱線しようと思いますが、「nil」について話す場合、値とリテラルを区別する場面もあれば、一緒にしても支障がない場面もあります。
例えば、「let v = 1」と書いた場合、この「1」というリテラルは「値」として捉えることができます。ただし、厳密にはリテラルというものは値ではなく、ソースコードに具体的に埋め込むための表記です。リテラル自体は特定の型に所属しておらず、どんな型にもなり得る特殊な表記です。例えば、
Int
型に入れれば
Int
の値になりますし、
Int8
に入れれば
Int8
の値になりますし、
NSNumber
に入れれば
NSNumber
の値になります。
具体的には、このリテラルがどの型になるかは、その文脈や左辺の型情報によって決まります。例えば、
NSNumber
に「1」というリテラルを入れる場合、この
NSNumber
ExpressibleByIntegerLiteral
プロトコルに準拠しているため、「1」というリテラルは
NSNumber
として扱われます。このプロトコルにより、適切なイニシャライザーが使用されてリテラル値が適切な型のインスタンスに変換されるのです。
同じように、「nil」もリテラルとして扱われます。例えば、
Int?
Int
のオプショナル)に「nil」を代入する場合、「nil」自体はどんな型にもなり得る特殊なリテラルです。ここで、
Optional
型の定義を見ると、
Optional
ExpressibleByNilLiteral
に準拠しています。つまり、
Optional
は「nil」リテラルを使ってインスタンスを作成することができるのです。
このプロトコルの定義を見ると、パラメーターとしてボイドを取る「init(nilLiteral: Void)」を要求しています。コンパイラは内部で
Optional<Int>
ExpressibleByNilLiteral
に準拠していることを確認し、「nil」からオプショナルインスタンスを生成します。
これが「nil」の基本的な仕組みです。このようにリテラルと特定の型のプロトコル準拠によって、リテラル値が適切に解釈され、適切な型のインスタンスが生成されるわけです。 「Twist」という名前のプログラムについて説明します。さっきは
Int
型のリテラルからの変換の話をしましたが、今回はオプショナル型との違いについて少し紹介します。
まず、オプショナルの
Int
型についてですが、シンプルに一般的な書き方をすると、
Int
のオプショナルに
nil
を入れると、それが
Int
のオプショナル型の値の
nil
として存在します。言い換えれば、それは「存在しない」ということです。しかし、こうやってインスタンスが作られると、型推論で完全に特定できない状況になり、これがコンパイルエラーとなります。これは
nil
の特徴というよりは、総称型(型パラメータを取る型)に関する問題です。
例えば、配列リテラルの場合は良しなに推論してくれますが、独自の型を作成する場合や、型パラメータを設定する場合は、これがうまく動作するかの確認が必要です。特に、リテラルの特徴として特殊な状況が生じます。
次に、型推論の例を見てみます。例えば、構造体を作成し、
extension
value
に対して、フェアローバリューが
Int
の場合の初期化子を作ります。これにより、コンパイルが通るか確認できます。この初期化子を使った時点でローバリュー(パラメータ)は
Int
であると推論でき、正常に動作します。こうした工夫により、型推論が可能になります。
また、
nil
の場合は、そのままでは型の特定が難しいため、何らかの方法で型を明示する必要があります。
ここまでの内容で何か質問があれば、お知らせください。
さて、オプショナルの問題ですが、例えば
W
型のオプショナルを任意の個数返したい場合、
nil
を複数個戻す関数を設定します。この際、
repeating
関数を使って次のように書きます。
Array(repeating: nil as W?, count: 5)
このように書くと、
nil
をオプショナルで5個返すことができます。パラメータ指定などの場合にも適切に扱う必要があります。
しかし、特定の条件下でリピーターを使う場合に、パラメータの指定に注意してください。このような工夫により、型の問題をクリアし、正常に動作するコードを書くことができます。 最初に試したのですが、ダメだと思ったことがありました。記憶に残っていると、何度も思い出して再挑戦するのは良いですね。編集をしているときに勘違いして、できないものだと思い込んでしまうことも結構ありますね。
先日、ダブルハテナ (
??
) を使う話をしていましたね。同じ理論で大丈夫そうです。Swiftの中ではリピーティングが一番きれいなやり方だと思いますが、あまり見慣れないので注意が必要かもしれませんね。型パラメーターを推論してくれる機能もありますね。どう名刺的に指定するかのバリエーションもあります。
推論させる部分がポイントですね。セルフを使って解説した事例がありました。型推論の状況によっていろんな推論の場所があるので、この部分を書けると便利です。
型推論が重要で、いろんなバリエーションを試しておくと良いことがありますね。コメントでもっと詳細に説明するとさらに良いです。
Swiftでは、型パラメーターやオプショナル型の処理方法など、さまざまな表現方法が存在します。具体例として、プロパティやイニシャライザー (
init
) の中でバリエーションを持たせる方法があります。
状況によって、どの表現方法が適しているか変わってきます。いろんな可能性を試してみるのは、いつか役立つことがあります。特に、フルで書く方法が適している場面や、反対に簡潔に書くほうが見やすい場面など、状況に応じた選択が大切です。このようにいろいろ試してみることで知見が広がるので、おすすめです。 他に何か面白い企画とか思いつく人はいますか?トータルで見て、こういったところの内容は生じてもそうですし、そもそもリピーティングの頃から一部など、動的なサイズでアップしていくリピーティングとなると、詰め直す感じになりますよね。型が出てきてリピーティングで詰め直せば、このようなノリになりますね。範囲を活用すればいけるんだと思いますが、リピーティングでここで材料を与えるわけです。ダブルのオプショナル型の値を用意して、ここに対して範囲と合わせます。しかし、あまりスマートに感じないこともあるかもしれません。これはあくまでもリピーティングを使ったときの表現方法の一例です。他の方法もあると思います。
例えば、0から5までの範囲をシーケンスに連ねる必要があったりしますが、このタイミングでいろんな方法が考えられます。オプショナルを使う例もありますが、これはあまりお勧めできません。0から5の範囲では、多分インデックスが0からであることを確認しないといけないです。実際のアプリケーションではどのように動作するか、検証が必要です。
さらに、ジップを使う方法もあります。たとえば、0と5という範囲とシーケンスを連ねる場合、異なるタイミングに入ってしまうことも考慮しなければなりません。ここではプリントしないと動作が分からないかもしれません。この方法は時折、ランダムに使える場合もありますが、実際のコードでは見直しが必要です。
また、nilを使う方法やReduceを応用する方法も考えられますが、実装によってはやりすぎになったり、意図しない挙動になることがあるので注意が必要です。例えば、ランタイムに依存する要件であれば、コンパイル時にエラーを検出しておくといった発想も重要です。ローカルにするとテストが大変になったりするので、実際の使用時には慎重な設計が求められます。
以上、今日はこれぐらいにしておきますが、次回はこの話の続きをする予定です。今日の勉強会はこれで終わります。お疲れ様でした。