https://youtu.be/snEckkCNK-4
今回は The Basics
のタプル
を眺めていきます。タプルは Swift 的には売りの新機能なのでこれまでも About Swift などで見てきたタプルですけれど、それだけ大事な機能な位置付けのはずなので、今回も改めてタプルについて The Basics 目線でみていけたらいいなと思います。よろしくお願いしますね。
—————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #130
00:00 開始 00:10 今回の展望 01:18 タプルとは 02:03 複数の型を混在可能 02:42 タプルは固定長 03:32 タプルと構造体の特徴を比べてみる 04:24 複数の値を複合して扱う 07:31 配列と違って固定長 08:01 タプルと構造体のメモリー配置 09:15 構造体とタプルを振り替える 10:44 unsafeBitCast はよく使うもの? 13:00 Swift が安全性を担保する機能 14:13 ImplicitlyUnwrappedOptional 15:22 強制アンラップ 16:32 if let 省略表記 17:25 強制アンラップよりも guard が想像を広げる感じ 19:17 早期 Exit の選択肢 19:49 アプリケーションをプログラムから落とすことについて 21:54 Swift が安全性を担保するコードを選んでいく 22:54 unsafeBitCast における安全性の検証 24:11 unsafeBitCast はバッファーオーバーランから保護する 25:32 名前付き型と非公称型 26:54 公称型と型拡張 29:28 タプルによる HTTP ステータスコード表現例 30:44 タプル表現についての補足 32:10 タプルの要素は何個まで? ——————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #130
では始めていきましょう。今日は Tuple のお話ですね。Tuple はあちこちで話題に上がって、その都度いろいろ細かに見ていたので、長く参加してくれている人にとっては結構おなじみな話題かなと思いますが、その頃に出ない方もいるでしょうし、出てくるたびに触れておくのも結構重要かなと思います。「The Basics」の流れに沿って Tuple について見ていこうかなと思います。
Tuple というのは Swift にとってはもう10年前とかになりますが、新機能として大々的に取り上げられました。Objective-C が Tuple みたいなのを持っていなかったというのもあるんでしょうけど、そんな感じで結構 Swift にとっては推しの機能だったので、The Basics にどんな話が出てくるか楽しみですね。
まずは軽く見ていくと、Tuple というのは複数の値を一つの複合的な値にまとめるものです。インスタンス目線で見ればその通りですし、型の目線で見れば複数の型を複合的にまとめて一つの型にできるとも言えます。どちらも間違っているわけではなく、型をインスタンス化する流れで複数の型がまとめられて、それをインスタンス化したら複数の値がまとまったインスタンスになるという感じです。
もう少し読んでみると、Tuple で扱う値はどんな型でもよくて、複数のまとめた値それぞれの型が同じである必要もありません。ここでは C言語や一般的な配列型と比較しての話ですね。配列は同じ型の要素を複数まとめて扱いますが、Tuple はどんな型でも混ぜることができます。
ちなみに、Swift の配列はランタイム時にサイズが変わる動的配列を採用していますが、Tuple は実行時にサイズが決まっている固定長の要素を持つものです。この辺りが大きな違いかもしれませんね。
ここまで見てきて面白いのは、Tuple の特徴と構造体の特徴がほとんど一緒だということです。複数の値を一つの複合的な値にまとめるという点です。ただ、このフレーズには若干の違和感がありますね。構造体は複数のデータ型を使って、一つの総合的な値を作るわけです。本質的な役割としてはそうです。
例えば、以下のように構造体で書くとします。
struct HTTPStatus {
var statusCode: Int
var description: String
}
HTTPステータスを作るために Int
と String
型を使っているわけです。これが構造体です。
一方、Tuple だと以下のようになります。
let httpStatus: (code: Int, description: String) = (200, "OK")
print(httpStatus.code) // 200
print(httpStatus.description) // "OK"
HTTPステータスが二つの値でまとまっているうちのコードを取り出す、ディスクリプションを取り出す、といった感じですね。
いずれにしても、複数の型をまとめて扱うという点では構造体も Tuple も一緒です。 それぞれ別々の型でなくてもいいのですが、とにかく一つにまとめている、という感じです。配列と違って、固定帳というのはイントとストリング、型を抜きにすれば二つの要素です。これが三つになることはありません。こういう固定帳、構造体も一緒ですよね。保存型プロパティが増えることはない、仕様が終わっちゃえばね、というところは全く同じです。でも実際、全く同じなんですよ。
Swiftのメモリレイアウト周りの資料にも、構造体とタプル型のメモリ配置は一緒みたいなことが書いてあったと思います。それを仕様と捉えるべきか、現状そうなっているだけなのかは分かりませんが、メモリ配列が全く一緒ということは UnsafeBitcast
で振り替えることができます。例えば、今 HTTPステータスはタプル型ですよね。タプル型になっていますが、これをHTTPステータス型の構造体として UnsafeBitcast
でHTTPステータスタプルで作ったインスタンスをHTTPステータスに振り替えることが問題なくできて、実際に動きます。こうやって振り替えてみると、実際にXのコードとXのディスクリプションはちゃんと200のオッケーで取れています。
この話が仕様かどうかは分かりませんが、基本的には構造体とタプルのメモリ配置が同じだと理解しておくと良いでしょう。そのため、この UnsafeBitcast
で同じ構造のタプルをストラクトに置き換えるキャストは可能です。実際にやってもいいと思われますし、同様に同じ構造のタプルに対してHTTPステータス型の値をタプル型に振り替えることも間違いではないと思います。あまり頻繁に使うことはないかもしれませんが。
例えば、ラベルをつけていないので0と1で対応するわけですが、こう振り替えられるんです。 UnsafeBitcast
を使っているけれど、安全に振り替えることができる場面があるかもしれません。興味がある人は適切な場面で使ってみると良いでしょう。ただ、業務系のコードではあまり推奨されないかもしれないですね。
業務系で UnsafeBitcast
をよく使うかと聞かれると、そうではありません。やはり業務系ではあまり使わないですね。 Unsafe
というキーワード自体がもう危険だと言っているわけですから。
この Unsafe
な操作はプログラマーが安全性を担保すれば問題ないですが、通常のフローや設計上ではカバーすべきではない場合もあると思います。性能が必要な場合などに使うことがあるかもしれませんが、それもパフォーマンスがどうしても必要なときに限られます。
自分の経験上、iOSアプリの開発でパフォーマンスに困ったことは音声処理ぐらいですね。そういったクリティカルな場面では UnsafeBitcast
の出番があるかもしれませんが、ユーザーインターフェースを操作するような場面ではそこまで困ることはあまりないですし、より安全な方法を選ぶべきです。
今回の例では、構造体に統一してしまえば UnsafeBitcast
の必要がない状況です。だから、 UnsafeBitcast
というのはSwiftが安全性を担保しないというだけで、プログラマーが安全性を担保すれば良いのですが、できるだけ別の方法を選ぶのが得策です。
例えば、Implicitly Unwrapped Optional のように、iOSアプリでよく見かける IBOutlets
を使って、UI要素に割り当てるときなどです。これも実行時には値が入ることが保証される場面で使うものですね。このように、安全性を担保した形でコードを書くことが基本となります。
結局は適切な方法を選び、コードの可読性と安全性を高めることが重要です。 昔は強制アンラップのオプショナルがありましたが、今はこれがフラグ扱いになり、暗黙的にアンラップするオプショナル、通称 IUO
(Implicitly Unwrapped Optional)と言われています。IUO
属性付きのオプショナルです。IUO
という言葉が出てきたら、Implicitly Unwrapped Optional、すなわち強制アンラップなんだということを思い浮かべてもらえれば、Swiftの話が通じやすくなります。
この IUO
を使っているために表に見えにくいですが、これはSwiftが安全性を担保しない機能の一つでもあります。例えば、普通のオプショナルなら !
(ビックリマーク)で強制アンラップをしますよね。強制アンラップは稀に使う場面があります。昔はSwiftの登場初期には、この !
が市民権を得ていなくて、使うべきではないと強く言われていた時代がありました。しかし、現在ではオプショナルだけど値が入っていることが担保されている状況であれば使っても良いという雰囲気になっています。とはいえ、強制アンラップをしないで済む状況が作れるなら、そうするべきでしょう。
最近のXcodeは補完機能が非常に優れており、次のバージョンではこのコードの書き方がさらに簡単になる予定です。ベータ版で新しい書き方を試していると、慣れないためにわかりにくいと感じることがありますが、これは使い続けて慣れるしかありません。
ガード文を使うと、ランタイムエラーを避けるための安全な方法も考えやすくなります。例えば、!
を使わずにエラーをスローする、もしくはガード文を使って良い状況に対処するなど、選択肢が広がります。これにより、安直に強制アンラップを使うよりも考える余地が生まれ、結果として安全なコードを書くことができるのです。
例えば、ループの中であれば continue
や break
を使うことができますし、関数内なら return
もあります。ランタイムで落としたければ fatalError
、エラーハンドリングに回したければ throw
など、用途に応じた適切な対処法が考えられます。iOSアプリでは exit
を使うと審査に通らないことが契約に含まれているので注意が必要です。
逆に、macOSアプリでは exit
を使うことは許容されているようです。アプリを閉じてプリファレンスペインを開くような動作も見られるので、macOSでなら大丈夫だと言えるでしょう。 もしMac OSアプリの話が出てきたときは、アプリを終了させる機能を考慮に入れても良いかもしれません。具体的には、CocoaやAppKitが使えれば、NSApp.terminate(sender)
によってアプリを終了させることができます。他にもNSWorkspace.shared
にもterminateのメソッドがあるかもしれませんが、詳細は忘れてしまいました。とにかく、ちゃんとしたAPIが存在するはずです。
Swiftは安全性を担保する機能を提供してくれるので、例えば未初期化の変数に対して次の処理が進まないなど、プログラマーに注意を促してくれます。初期化を遅らせる機能もありますが、これも一種の安全機能です。これにより、必要なタイミングで初期化することができ、安全性を高めます。
一方で、unsafeBitCast
は、一部の安全性を担保していますが、基本的には危険です。たとえば、型変換時にメモリレイアウトのサイズが異なるとランタイムエラーになる可能性があります。このようなエラーはビルド時に検出されることが少なく、ランタイムで発生することがあります。しかし、unsafeBitCast
はコンパイル時にエラーを出す場合もあります。
unsafeBitCast
の提供する安全性は、主にバッファオーバーランを防ぐことです。バッファオーバーランは、変数に対してメモリサイズを超えるデータを書き込むことで、関数のエントリーポイントを変えてしまうなどの脆弱性を引き起こします。このような脆弱性を防ぐためには有効ですが、Swiftの言語的な安全性とは少し異なります。オプショナル型の検査など、Swiftが提供する他の安全性とは別の視点になります。
さて、Tuple(タプル)と構造体の違いについてですが、これらは似ているようで大きな相違点があります。タプルは匿名型(名前なし)で、構造体は名前付きの型です。タプルはノミナルタイプ(nominal type)ではなく、ノンノミナルタイプ(non-nominal type)と呼ばれます。ノミナルタイプにはエクステンションが可能ですが、ノンノミナルタイプにはエクステンションができません。したがって、Httpステータスのようなノミナルタイプには好きな関数を実装できますが、タプル型には拡張することができません。
この違いによって、タプルを構造体の代わりに使うと、例えばカスタムストリングコンバーティブル(CustomStringConvertible)に準拠させるようなことができません。このような制約があるため、タプルと構造体は使い分けが必要です。 したい場合には、どうしてもカスタムな表示にしたいときには構造体を使用することになります。名前付きタイプなどの言葉を使うこともありますが、ここではタイプエイリアスを使って名前を付けるという方法も一つの選択肢です。ただし、これはあくまでも仮の名前を付けているだけで、名前付きの型になったわけではありません。名前を付けることで少し便利になる程度です。
例えば、ラベルが付いていないときに15行目や16行目でエラーになってしまう場合、名前を付けてあげればエラーを回避することができます。ただし、これでもノミナルタイプ(名前付き型)にはなりません。そのため、エクステンションを使用してステータスを追加しようとしても、コンパイラからあくまで単なる別名であるためにノミナルタイプではないからエクステンションが追加できないと言われます。
この点が構造体とタプルの似ているけれど異なるところです。例えば、Httpステータスコードを表現したタプルの例があります。Httpステータスコードはウェブサーバーにリクエストを送った際に返される特殊な値です。例えば、「404 Not Found」は、要求したウェブページが存在しないときに返されるステータスコードです。
タプルは非常に柔軟で、異なる型を含めることができます。例えば Httpステータスコードの場合、整数型と文字列型を組み合わせて一つのタプルにすることができます。タプルの型は (Int, String)
という形で表現されます。
さらに、タプルの型は任意の型の組み合わせで作成でき、その数も制限がありません。たとえば、255個の要素を持つタプルを作ったことがありますが、一般的にはそんなに多くの要素を持つタプルを使うことは稀です。
次に、大量の要素を持つタプルを試してみます。まず10要素のタプルを作り、それを20要素、100要素、さらには2000要素にも拡大してみました。コンパイル自体は問題なく通りましたが、インスタンス化する際にはメモリの制約により動作しなくなることがあります。特に大量の要素を持つタプルをスタックに格納しようとすると、スタックオーバーフローが発生する可能性があります。
今日の勉強会はここまでにしましょうか。お疲れ様でした。ありがとうございました。