https://youtu.be/CKQuZGrsS8k
今回はThe Basics
の 数値型変換
の中に記されている 整数と浮動小数点数の変換
について眺めていきます。これまでに見てきたデータサイズによる表現範囲の幅を吸収するのと違って、データサイズは同じでも表現そのものの仕方が違う型同士の変換が着目ポイントになっていそうです。それとそんな話の前に、前回の話の最後で自分が混乱して説明できなかった、型変換とイニシャライザーの関係性的な余談についても触れられたらいいなと思ってます。どうぞよろしくお願いしますね。
———————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #123
00:00 開始 00:13 整数における型変換のおさらい 00:49 今回の展望 01:41 前回の訂正に向けたあらすじ 08:08 イニシャライザーを求めるプロトコルをクラスに適用 08:46 必須イニシャライザーが要求される 11:42 クラスを想定したプロトコルで変換イニシャライザー相当の機能を要求する 13:39 クラスで Self を戻り値として返す 16:23 静的メソッドが必要になった体験談 20:16 整数と浮動小数点数の変換 20:24 整数型から浮動小数点数型への変換 21:02 整数から浮動小数点数への変換の具体例 22:51 浮動小数点数への変換が実現されている仕組み 23:24 浮動小数点数から整数への変換の具体例 23:58 円周率 = 3 ? 26:39 値を保全する型変換の意味合い 29:07 値を保全しない型変換の意味合い 33:25 変換できる型の探しやすさ 35:30 変換が必要と気づいた時の手戻り感 38:38 直感的にわかりやすい変換構文 39:47 クロージング ————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #123
はい、では今日は型変換の続きをお話しします。今までは整数の型変換について、int
型には様々なサイズ(例えば、int32
やint64
など)があり、それぞれ表現力の差があるという話をしてきました。そのため、プログラムを書く際にはどの型に合わせるかを明確にしなければならないという話でしたね。
今日はサイズの違いではなく、整数型(Int
)と浮動小数点型(Double
)のように、同じ64ビットでもビットの評価の仕方が全く異なる型について、その変換方法について学びます。このスライドを見る限り、そのような話題になるかと思います。
その前に、前回話した内容について少し補足させてください。具体的には型変換とイニシャライザの相性についてのお話です。前回、他の言語にありがちな例として、例えばInt
型に変換する際にtoInt
関数を使うという話をしました。しかしSwiftでは、イニシャライザを使って他の型のインスタンスをInt
型に変換する方法が推奨されます。
例えば、他の言語では次のように書きます。
let intValue = someValue.toInt()
しかし、Swiftでは次のように書くのが一般的です。
let intValue = Int(someValue)
このようにイニシャライザを使用するスタイルは、APIデザインガイドラインでも定められています。1行目のスタイルは基本的に推奨されていませんが、プロトコルを使用する際には必要になる場合もあります。この点について前回説明しきれなかった部分がありますが、今日はその訂正と補足を行います。
話を元に戻して、整数型と浮動小数点型の変換について進めていきましょう。いずれも異なる方法でビットを評価するため、型変換する際には注意が必要です。それでは、このテーマについて詳しく説明していきます。 プロトコルを使って、特定の型変換を強制するときに、指標が出てくるというお話でした。具体的には、何らかのデータから整数(Int
)を作るときに、そういう機能を提供する方法についてです。例えば、Value
型というのがあって、これをMyValue
にしましょう。以前に話した「整数を作る話」とは逆に、今度はデータから変換する話になります。
例えば、ある型があって、その型のインスタンスをデータから生成するような処理が必要です。このような処理をプロトコルを使って義務付ける場合、InstantateFromData
というプロトコルを作成し、これを用意した型に準拠させます。そのためには、イニシャライザを提供して、そのイニシャライザがデータからインスタンスを作成できるようにします。
具体的に言うと、MyValue
という型に対して、InstantateFromData
プロトコルをエクステンションで準拠させ、イニシャライザを定義します。このイニシャライザは何らかのデータからMyValue
のインスタンスを作成する役割を持ちます。
同様に、Int
型にもInstantateFromData
を拡張し、イニシャライザを提供すれば、データからInt
を生成することができます。これによって、MyValue
型でもInt
型でも、汎用的にデータから変換できるようになります。
一方で、NS私番号
のような既存のクラスにこのプロトコルを準拠させようとすると、プロトコルに要求されるイニシャライザがrequired
である必要が出てきます。プロトコルが要求するイニシャライザは、クラスの場合、必ずそのクラスの定義においてrequired
イニシャライザとして定義しなければならないからです。ただし、クラスをfinal
にすることでこの制約を回避できる場合もありますが、NS私番号
などの既存クラスではこの方法は使えません。
この部分は以前の勉強会でも話したことがあるので、興味があればそちらも参照してください。とにかく、プロトコルが要求するイニシャライザは、クラスの定義においてrequired
として定義する必要があるので、その点を注意してください。
このようにプロトコルとイニシャライザの関係について理解すれば、より安全かつ効率的に型変換を行うことができるようになります。 型変換について話し続けますが、他の型に変換するというよりも、他の型から変換する際にプロトコルが介入してくる場合があります。プロトコルが構造体だけを対象としている場合は問題ありませんが、クラス型も対象としている場合、APIデザインガイドラインが求めるイニシャライザーの表現がうまくいかないことがあります。
このような場合、昔の手法でよく見られるのが、現在も場合によっては積極的に使われているかもしれませんが、静的関数(static function)として make
などのメソッドを作成する方法です。たとえば、static func make(from data: Data) -> Self
のように定義すると、適切に動作します。このようにすると、ある型から他の型へ変換するためのメソッドが定義できます。
具体的な例として、MyValue
を返す方法について説明します。この場合、型 Int
に対しても同じ要領で static func make(from int: Int) -> Self
を使用し、戻り値として Int
型を生成して返すことができます。これにより、イニシャライザーではない形で Int
型から変換を行うことができ、コンパイルも通ります。
また、NSNumber
の場合も同様に static func make(from number: NSNumber) -> Self
を使用しますが、セルフを返すのが難しい場合があります。その理由として required init
が必要で、将来の継承先を考慮しなければならないからです。このような制約があるため、クラスの型変換の実装には注意が必要です。
Swift では、型変換の際にイニシャライザーを使用して安全性と利便性を高めることが推奨されますが、場合によっては静的関数を使用することも選択肢となります。特にクラスを扱う場合、イニシャライザーだけで対応しようとすると制約が多いため、その点に留意が必要です。
このようにして、型変換を行う際の設計について理解を深め、適切な方法を選択することが重要です。古い型変換の流儀では静的関数を頻繁に使用していたため、現在の API デザインガイドラインに沿ってイニシャライザーを用いる場合、過去の設計方法から変更する際に注意が必要です。特に、Foundation フレームワークのブリッジの都合もあって、クラスをよく使用していた場合など、適用できないことがあります。そのため、クラスを想定している場合、イニシャライザーを求めることが適切であれば良いですが、そうでない局面では静的関数を使うという選択肢も考慮するべきです。この点を押さえておくと、設計の際に役立つでしょう。
以上を踏まえ、型変換に関するAPIデザインガイドラインを理解し、適切な方法を選択することが大切です。 ここまでで大丈夫でしょうか。他に何か思い出話などがあれば、ぜひ聞かせてほしいと思います。
ところで、スタティック関数でインスタンスを作成するというのは、最近ではあまり見かけなくなりましたね。これは自分の気のせいでしょうか。Objective-Cの時代にはよく行っていたように思います。これはARC(Automatic Reference Counting)の影響でしょうか。ARCが登場した時にもそうでしたが、それ以前の手動リファレンスカウンティングの時代もそうでした。イニシャライザーで宣言した場合、それを受け取った側がリテインするかどうかを責任持つというルールがありました。
例えば、String
の方が分かりやすいかもしれませんが、新しいストリングを別のストリングから変換する場合、生成したインスタンスを相手側がリテインまたはリリースする責任を負います。そのため、String
という関数がスタティックファンクションになり、この中でストリングを生成します。これがスタティックファンクションの流れで動くわけです。オートリリースを活用し、リテイン・リリースのタイミングが自動で管理されるようなガイドラインがありましたね。Objective-Cの時代には、このスタティックファンクションをよく利用していましたが、Swiftになってからはイニシャライザーに取って代わられました。
さて、本題に入りましょう。今回は整数と浮動小数点数の変換について学んでいきます。Swiftでは、これらの変換が明示的に行われるという点が重要です。これは、整数同士の変換と同様に、プログラマーが予期しない変換や丸めが行われないようにするためです。
例えば、以下のようなコードがあります。
var a = 3 // 整数型
var b = 3.14159 // 浮動小数点型
この場合、a
は整数型で、b
はダブル型です。ここで、a = a.b
のようにすると型が違うというエラーが出ます。このように、変数の変換は明示的に行う必要があります。
以上が、Swiftにおける整数と浮動小数点数の変換の基本的な考え方です。次に進みましょう。 確かにこなす必要があるんです。あとは、何か問題が起こったときにそれを防ぐのが大きな目的になるかもしれません。例えば、変数 A
に 3
を代入するという例を見てみましょう。これはあくまで例なので、他の変数名を使っても良いのですが、A = 3
、B = 3.14159
とします。この場合、A
が整数型(Int)で、B
が浮動小数点型(Double)です。
一般的に、このような場合は整数型を浮動小数点型に変換して実行します。例えば、A
を浮動小数点型に変換すると、3.0
という値が得られます。これは、Swiftが整数型を取るイニシャライザーを浮動小数点型に用意しているためです。具体的には、整数型を受け取るイニシャライザーが浮動小数点型に備わっているので、このような変換が可能になります。
この過程を踏むことで、Double
型の値を正しく得ることができます。逆に、浮動小数点型から整数型への変換も同様に可能です。例えば、Double
型の値を整数型に取り込むためのイニシャライザーも用意されていますので、3.0
を整数型に変換すると 3
という値が得られます。
最近、ツイッターで「円周率を 3
として教える」という話題がありましたが、実際にはこれは誤解で、結局は 3.14
と教えるということが分かりました。円周率を 3
と丸めてしまうと計算が無茶苦茶になりますよね。例えば、π
を3に丸めると、計算結果が大きく変わってしまいます。小学校では円周率を 3.14
と教えているので、適切な計算ができるようになっています。
型変換の際にはAPIデザインガイドラインに従うのが一般的です。例えば、値を保全する型変換の場合にはラベルを付けない、値を保全しない明示的な解釈を含む変換の場合にはラベルを付けるなどのルールがあります。
以下の例を見てみましょう。5.0
というダブル型のリテラルが整数型に変換される場合です。
let a: Int = Int(5.0)
これは、ダブル型の値を整数型で保持するための変換が適切に行われています。このように、型変換の際のアプローチについては詳細に理解し、設計することが重要です。 例えば、5.0
は比較できないですよね。じゃあ、5.0
もダブルに変換してみましょうか。ちょっと分かりにくいかもしれないので、一旦イントに変換したものを再びダブルに戻して、元のダブルと比較してみると、値が保全されていると解釈できますよね。でも、もしイントが 5.5
になった場合、それを変換しても元に戻らないわけです。0.5
が落ちてしまいますよね。こういった状況が、値を保全するタイプコンバージョンだと言えます。
ラベルがついていない場合でも、値を保全するタイプコンバージョンは、表現可能な値の範囲内で元の値を実質的に保全していると言えます。例えば、定数の世界では小数点を表現できないので、丸める必要がありますよね。こういった場合も、値を保全するタイプコンバージョンと認識されます。
一方、値を保全しないタイプコンバージョンの場合、イント型で具体的に説明できます。例えば、エグザクトUは整数型の範囲を超えた値を扱う場合があります。この場合、値を保全するタイプコンバージョンと似た動きをしますが、ビットパターンとして扱うと異なる結果になることがあります。
例えば、Uイント
ではビットパターンを基に変換します。もし、ビットパターンで -1
とか Uイント
マックスを渡すと、Uイント
の表現力がイント型の範囲を超えている場合、結果は -1
になります。普通のイント型に対して Uイント
マックスを渡すと、オーバーフローしてしまうわけです。こういった場合、値を保全しようとしても、イント型がランタイムエラーを選んでしまいます。
W型
に対してはイニシャライザーが何があるか見てみると、ビットパターンによる解釈は難しいことがあります。例えば、ダブル型の 5.5
をビットパターンで解釈すると、全く別の値になってしまいます。これが値を保全しないタイプコンバージョンの例です。
ここでお話ししているタイプコンバージョンはラベルがついていない型変換です。値を保全する方向に働いても、表現の仕方による影響があります。2次元の値を3次元に変換するイメージかもしれません。具体的なコードを見てみると、Uイント64
の変換は、より効果的です。
以上が、値を保全するタイプコンバージョンとしないタイプコンバージョンについての説明でした。 変換を探すときに、ビットパターンからいきなり探すのは難しいですよね。発想としては、UInt
が欲しいからUInt
のユニットが対応するかどうかを考えることになりそうです。いろんな型を試してみることになるでしょうが、やはりユニットの方がわかりやすいです。
仕事と実用のどちらが重要かというと、変換がビットパターンで行える場合は一覧性が欠けるので、ユニットの方がわかりやすいです。今の過程を見ていると、なんでstatic
が関係しているのか明確にわかりませんでしたが、ユニットの方が理解しやすいと感じます。
確かに、自分も話を聞くまでは、ユニットを頭に入れておかないといけないと感じていました。しかし、変換という観点から考えると、型を合わせる必要がある場合でも、確実に変換ができるのはいいですね。
オプショナルの場合は連続してつなげられるのですが、オプショナルではない場合はどうなのか、ここが悩むところです。コードが増えた時にどうなるかはわかりませんが、選択肢としてはありそうです。
type conversion
というのは人間のわがままであり、戻りが嫌だからこそdotUInt
とか自動的に変換したいと思うわけです。これはイニシャライザーに統一感を持たせることで、確実に変換できる感覚が得られます。
ビットパターンからの変換を見つけやすくなるかどうかですが、例えばBitPattern.toUInt
のように書ければ、その後にdotUInt
が使えます。標準の型変換とのギャップがあるかもしれませんが、イニシャライザーを通した方がスマートですね。
昔からキャストという概念があったので、型変換の感覚がわかりやすいです。例えばtoInt
やtoDouble
といった名前付けで型変換を行っているのがわかるようにできます。
データパターンのtoInt
とかtoDouble
とかがあると、何をしているのかがわかりやすいです。最終的にはイニシャライザーに行き着く感じですね。手戻りが気になりましたが、それを考慮してもイニシャライザーの方が良いですね。
いい感じになったので、今回はここまでにしておきましょう。お疲れさまでした。ありがとうございました。