https://www.youtube.com/watch?v=_1gT7veg64U
今回は A Swift Tour
の「シンプルな値」から「型変換」について眺めていこうと思っています。前回に見ていった「型推論」についての簡単な練習問題もあるみたいなので、せっかくなのでそれを最初に見てから、型変換の話に入ってみることにしますね。
——————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #27
00:00 開始 00:25 CGFloat と Double の暗黙変換 02:13 import の必要性 05:24 CGFloat と Double が暗黙変換できる理由 08:20 Toll-Free Bridge 11:11 将来に Double と CGFloat のサイズが違ってしまう可能性は? 13:11 暗黙変換の導入によって型が統一されていきそう 13:50 CGFloat と Double の差を埋めるアイデア 15:21 型変換 15:51 型を特定するのに十分な情報がないとき 18:34 オーバーロードとリテラル変換 20:43 型を推定できない場面 22:04 整数リテラルを Double 型として扱いたい時 25:26 型推論の速度 29:04 変数を Double として扱いたい時 30:55 整数リテラルも Double 型になり得る 35:03 リテラルの種類の違いによる初期化プロセスの違い 39:23 リテラル表記の仕方で型を精密に切り替える言語もある 42:01 練習問題 44:08 問題文に忠実に解を考えてみる 45:49 いろいろな書き方ができることも大切 46:52 型を記載するときの特徴 48:49 次回の展望 ———————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #27
はい、じゃあ始めていきますね。
前回、型推論のお話をしましたけど、いきなりちょっと余談というか関連する話題でもありますけど、前回C言語、Float型とDouble型の互換性、透過的な型変換ができるよっていうお話を少し調べてみました。調べるも何も、ちゃんと見たらSwift Evolutionにしっかりプロポーザルとして出ていて、公式の対応だったということがすぐに分かりました。つまり、安心してとりあえず使っていいというものでしたね。それについてプロポーザルの方にもいろいろと書かれているので、理由とかも分かります。
この「ゆめみ」のSlackにも興味深い話が出ていて、そこではCGFloatの背景について詳しい人が解説してくれていました。私はそこまでCGFloatに詳しくなかったので、勉強になりました。おさらいすると、Double型としての値とCGFloat型の値をCore Graphicsにインポートしたときの互換性について便利だと言えるでしょう。
個人的に余談になりますが、インポートをわざわざ書くという文化、そろそろ時代遅れな感じがしませんか?使うたびに「ここでデータを使おう」と思ったときにimport Foundation
と書かなくてはいけないというのが少し面倒に感じます。ただし、これがないとどうにもならないこともあるので、まだ存在しなくてはいけない場合もありますね。特に名前空間的な問題が関わる場合はそうです。
例えば、モジュールAに規定されている何かと、モジュールBに規定されている何かが同じ名前であった場合、import B
の状態だとBのものが使われますが、import A
にするとAのものが使われるという風に、インポートを明記しないと名前空間の衝突が起こりえます。曖昧な書き方を避けるためにもインポートは必要ですね。
さて、前回のお話に戻ります。Core GraphicsのDouble型とCGFloat型が相互変換可能であることについてです。例えば、Swiftのprint
関数がDouble
型の引数を取る場合、CGFloat
型の値も渡すことができるという動きが確認されます。これは、引数に渡すときに暗黙的な型変換が働き、代入するときにも暗黙的な型変換が行われるからです。
CG型は高速に不動小数点数演算を行うための型であり、32ビットと64ビットのそれぞれの環境に最適なパフォーマンスを出すために使われていました。しかし、現在では32ビット環境でも64ビット環境でもDouble型で同等の速度が出るため、CGFloatを意識的に使う必然性が薄れています。そのため、この暗黙的な型変換が導入され、プログラマーにストレスを与えないように配慮されているとのことです。 この暗黙変換は昔から存在していて、NSString
型のテキストを…これ、書き方を間違えましたね。NSString
型の書き方は多分できますよね。ちょっとちゃんと試してないけど、CFString
のSに逆じゃないとダメとかあるのかな?まぁ、大丈夫ですよね。
こういうふうなCore FoundationとFoundationの透過的なブリッジっていうのも今まで既に存在していた。これと似たようなものだというふうに書いてありました。知る人ぞ知るNSString
とCFString
っていうのは「トールフリーブリッジ」といって、型キャスト、いわゆるC言語的に何も配慮しないでも変換できるという性格があるので、CGFloat
とDouble
型みたいに32ビットだったり64ビットだったり、そういう再表現による再表現みたいなものはないんですけど、トールフリーブリッジはね。
だけど、こういう暗黙的な配慮を今までSwift言語がしれっとやってたというものがあるので、特別なことじゃないよという主張がされていましたね。ちょっとトールフリーブリッジを実際に試してみたいな。
U = CFString = unsafeBitCast(NSString.self, to: CFString.self)
、トールフリーブリッジいけるよね。print(U)
、問題なくバグやアクセスエラーは起こらずにいけるでしょ。このポインターを振り返るだけでブリッジできるのがCFString
とNSString
みたいな大事なポイントになってる。そういう設計になってる。
コメントで心配されている通り、128ビットが登場してDouble
型の演算がもし128ビットの方が早いよみたいになった時には、この暗黙変換がね、またテコ入れされなきゃいけない場面に出てくるはずですね。その時のテコ入れのされ方というのもまた、雰囲気がきっと変わってくるんじゃないかなとは思うんですけど、とりあえずね、そういう128ビットとか将来もね、踏まえた時にどうなっていくか分からないけど、現時点では最適な回としてこの暗黙型変換というものが存在できるよという形で入ってるみたいです。
そこは大事なところでね、将来的にどうなるかは分からないけど、今はこれでいいって。
でね、このCore Graphics自体もApple独自のプラットフォームであって、今のスタンダードはDouble
に変わりつつあるっていう状況を踏まえても、将来128ビットが出た頃にはCGFloat
がそもそも使われてないっていう現状が起こり得る。そういうふうに考えていくと、まあなかなか妥当な判断をしてるのかなっていう気はしますけど、Swiftのね、方に馴染んでる、Swift言語の仕様に馴染んでる自分としては気持ち悪い。非常に気持ち悪い。
まあでも、そのうち自然とDouble
だけになっていくんでしょうね。つまり、Double
型でもCGFloat
型でも、必要に応じて勝手に変わっていくっていう風になることによって、ソースコードがね、自然とDouble
型での定義だけになっていくと思うんです。わざわざCGFloat
で定義する人っていうのは多分減ってくる。どんどん。
そうすると自然とね、CGFloat
が投下されていくという方向にも向かっていく。なかなか面白いアプローチだなと、今回これを見ていて思いました。
ちなみにSwiftUIとかでDouble
型に統一すべきだみたいな話を前回自分がしましたけど、それも考慮済みで、4つぐらいのパターンがね、想定し得るパターンが挙げられていて、最終的にこの透過的な暗黙変換が最適だろうという形で選ばれているそうです。
面白いですね。だからやっぱりね、このあたり、自分としてはおかしく見えるけれど、Swiftが選ぶならそうなんだろうみたいな、そのSwiftを信じるっていうのは大事だなと改めて思いました。まあね、Swiftも間違うことあると思うんですけど、その時はまた直していくと思うので、そういう意味でもSwiftを信じていくっていうのはいい選択かなと思います。まぁ宗教みたいなもんですね。盲信するんじゃなくてね、信じてそれを尊重してってことになるんですかね。余談が長くなりましたが、そんなお話をしました。
今日は型変換の話になるんで、一応ね、これも前提としてイメージ持っておくと、多分前回参加してくれた人は違和感なく聞けると思います。型推論のお話を前回して、ここも前回の話で補えてるのかな。十分な情報がない時には補う必要がある場合もありますが、今回はそんな感じですね。 この今表示されているサンプルコードの3行目ですが、ダブル型の定数を宣言したいとします。しかし、リテラルとして「70」をそのまま使ってしまうと、この場合はバグの原因になることがあります。要は、ダブル型の定数を宣言したいのに、文脈から見てそれがダブル型ではない場合、コンパイラがダブル型と判定できないときには、型を明記する必要があります。
例えば、let value = 70
と書くと、これはイント型として扱われます。先ほどの勉強会でもお話したように、整数リテラルは文脈から型が判断できないときには、デフォルトでイント型になります。
今回のこの23行目は、リテラルに関する話と型推論についての話を両方理解していないと難しいお話になっています。何かのメソッドがオーバーロードされているとき、例えば、イント型を取るメソッドとダブル型を取るメソッドがあった場合、リテラルを引数として渡すと、デフォルトのリテラルの型が決定し、それに応じたメソッドが呼ばれるという状況です。
ここまでの話からわかるように、文脈によってどのメソッドが呼ばれるかが変わってしまうため、明確に型を指定することが重要です。たとえば、let x = 70
をダブル型として扱いたいときは、次のように書きます。
let x: Double = 70
または、不動小数点リテラルを使用して次のようにも書けます。
let x = 70.0
プロパティの定義でも同様の書き方が適用されます。例えば、ストラクトでプロパティを定義する際には、以下のように書きます。
struct Value {
var value: Double = 70
}
これらの方法は、型を明確にすることで、予期しない型変換やバグを防ぐのに役立ちます。
この3行目の初期値が十分な情報を提供していないときには、型を特定する必要があるというお話でした。再度、let x = 70
をダブル型として扱いたいときには、次のように書きます。
let x: Double = 70
この他にも、変数の定義時やプロパティの定義時に適切な型を明記することで、コードの可読性や安全性を高めることができます。 とりあえず定数を宣言するときに、以下のような書き方をする人もいるんじゃないかと思います。これは let constant = 70.0
とやっていることは何も変わりません。ただ、リテラルなので少しわかりにくいかもしれませんが、ダブル型の70を作っていることになります。だから、不思議な書き方ではなく、むしろ普通の書き方だと思います。しかし、型を明示した方が良いという意見もありますね。
イコール70にしているという発想もあるんですね。型推論の処理のアルゴリズムの都合でしょうか。型推論は重いと言われていますが、その影響はわずかです。ただ、コーディングルール次第ですが、例えば配列や辞書型の場合は型の明示が推奨されます。昔、Swift 1, 2の時代に2000項くらいの配列や辞書型を書いた際、型明示しなかったためにビルド中にセグメンテーションフォルトが出てしまうことがありました。こういったときは型明示するしかありません。
例えば、クラスBがAを継承し、Aだけが StringLiteralConvertible
に対応し、Bが IntegerLiteralConvertible
と ExpressibleByFloatLiteral
に対応している場合、この全体をAの配列と推察するまでには時間がかかるでしょう。そういった潜在的な問題を含んでいるため、型を明記した方が早いという発想は理解できます。
不動小数点数リテラルに関しても、変数をダブル型として扱いたい場合、2つの方法があります。1つは「70.0
」と書く方法、もう1つは「70 as Double
」と明示的に型を指定する方法です。これについては、どちらで書いても処理の重さはほぼ同じで、重要なのはコードの読みやすさです。
C言語などに親しんでいる人は「70.0
」と書かないとダブル型と見なされないという時代を経験しているため、型にこだわる人もいるかもしれません。プログラム内で変数X(ダブル型)に10を足すときにも同様です。「X = X + 10.0
」と書かないと気持ち悪いと感じる人がいるでしょう。
現代ではこうしたこだわりを持つ人は少なくなってきていますが、それでも型を明確にすることはコードの可読性やメンテナンス性を高めるために重要です。 とりあえず、こう書いても何ら問題ないというか、Swift的には全く同じです。不動小数点数リテラルがあると、この型が何かを判定しようとします。しかし、10.0
だけだと決められないのです。ここが大事ですね。そのため、前後関係を見て、Swiftは基本的に2つの値の演算が両方同じ型であることを原則としています。この足し算を見たときに、左辺の変数 X
と同じ型であろうと判断し、この 10.0
をダブル型として変換をかける動きを取ります。
こっちも全く一緒です。右辺の 10
も型を定めたいのですが、この整数リテラル 10
だけでは型を決められません。前後関係を見て X
がダブル型なので、ダブル型に変換をかけるのです。ダブル型にはちゃんと整数リテラルからの変換イニシャライザーが用意されているので、これで変換をかけるのです。
さて、少し違っていましたが、このように変換をかける動きになります。この中でダブル型にリプレゼンテーションされるので、若干遅いかもしれません。実際にループをたくさん回して計算してみたくなりますね。あとでやってみます。
とりあえず、この説明が本当に正しいかどうかだけチェックしましょう。型を作ったっけ?作ってないか。まず試してみて、面白いコメントが添えられてきたので、Value
にエクステンションしちゃいましょう。Value
で ExpressibleByIntegerLiteral
で init
を添えて、integerLiteral
タイプは Int
にします。
この integerLiteral
に互換性のある型であれば、どんな Int
型でも取れます。例えば、ついでに説明しちゃいますが、Int
型って符号付きの64ビット整数であって、要は64ビットの符号なし整数を受け取れない状況になっています。しかし、整数リテラルというのは再現なく長く書けて、64ビットの符号なし整数も表現できます。これを受け取りたいときには UInt
と書くだけでちゃんと受け取れるのです。面白い仕様ですね。余談ですが、これで Int
型が完成です。
次にエクステンションで Value
に ExpressibleByFloatLiteral
を実装し、イニシャライザーとして FloatLiteral
を受け取れるようにします。普通、ダブルしか取らないですが、インテルプロセッサーなら float80
みたいな型もあります。このようにして受け取れます。ただし、ARMプロセッサーには float80
がないので、コンパイラーがエラーを出すかもしれません。おそらくM1だと float80
は使えないんじゃないかと思います。手元で試せていないですが、これで float
も受け取れるようになります。
次に問題の as
。これが value
の v
の value
で、1 as Value
。ちょっと自信がなくなってきましたが、ちゃんと動くか? int
として出ますよね。1.0
もこれで float
として出ますかね? 出ますよね。ちょっと混乱してきましたが、やっぱり切り替わりますね若干。
ですから内部操作次第でどっちも同じかと思って話していましたが間違いです。違いますね。ということは 70.0
の方がいいのか。モダンな書き方ではないですが、速さを気にするなら 70.0
の方がおそらく早いでしょう。自分は26行目を推奨します。26じゃないや、足し算の方がわかりやすい。足し算を消しちゃったっけ? 消してないや。また書き換えただけです。
つまり、x + 10.0
と x + 10
では、自分は 30
行目を推奨します。ただ、実際に 29
行目を選ぶ人の方が多分賢いです。
コメントで面白い話がありました。70.f
という書き方で float
を返すような仕様が見たことがありますが、こういう書き方もあります。70.0f
そうそう、これは Objective-C や C言語ですね。要は C++11 以降ではリテラルが強力になっていて、f
だと float
、d
だと double
というように、追加の情報をリテラルに添えて書くことができます。
また、文字列リテラルにも %ld
とか %d
といったフォーマットのような情報を追加することができます。このようなフォーマットもありますし、%ld や %d のようにリテラルにも情報を持たせる言語もあります。
このような味付けの仕方はありですが、Swift は型推論で味付けをしていく選択を取っています。それぞれの文化ですが、Swift はあまり特殊な書き方やローカルな表現(方言的な追加)はしていかない感じです。ただし、16進数や2進数、例えば 0x0
とか 0b0
のような表記はあります。8進数もあります。このような若干の方言的な書き方はあるものの、基本的には特別な書き方は極力避けている印象を受けます。 そんな風に型情報になっていこう、というお話がありましたね。Swiftツアーでは、練習問題が出てくるのが面白いと思います。それでは、とりあえずやってみましょうか。
「定数を作成してください。この時、型を Float
として明示し、その値を4とします。」
これくらいの問題だと多くの方は頭の中でも作れるかなと思いますが、どうでしょうか。難しいと感じる人もいるかもしれないので、もし「これ難しかったなぁ」と思ったら教えてもらえると嬉しいです。
問題の順番通りに書いていくと、このようになります。
let constant: Float = 4
最近の小学校では、こう書かないと罰をもらう風な噂も聞きますが、Float
と書いても正解です。また、4.0
と書いてあげても正解です。こちらの方が早いのかもしれませんね。
他に正解がありますかね?皆さんの考えを聞いてみたいです。素直な書き方としては、多分これくらいですね。
「Float
4リテラルです。」
問題文としては「リテラル4でしょ」という意味ですが、リテラル4を使って型を Float
に明示しましょうという話でした。
ただし、4.0
と書いた場合、型が Double
になってしまうので、明示的に Float
と書く必要があります。これもまた、教育現場での書き方によって考え方が変わるかもしれませんね。Twitterで炎上する未来もあるかもしれません。
この問題を通じて、いろいろな表現方法があることに気づけるのはいいことです。パフォーマンスが気になる人はパフォーマンスを追求すれば良いですし、見た目の美しさにこだわりたい人は整数リテラルを使うべきか、不動小数点リテラルを使うべきか、価値観で分かれると思います。
Javaでは Float
で宣言して new Float
でインスタンスを作るような書き方がありますが、Swiftでは後ろに書いても良いという感じですね。型推論ができるおかげで、どちらの書き方も可能です。Javaには型推論があるかどうかはちょっと把握していませんが、Swiftではイニシャライザーをスタティックメソッドのように呼べる特性のおかげで書けるのです。
みんなが63行目を書くときに init
を挙げてもらえたのは非常に良いですね。
こんな感じで今回入る予定だった型変換の話には触れられませんでしたが、時間の都合もあります。ただ、今回の話でも型変換についてはかなり詳しく説明できたと思います。
これは、あくまでもスライドを見せられなかっただけという感じです。次回の勉強会は1日あけて金曜日に復習ついでにスライドを見て進めようと思います。
基本的に今日はこんな感じです。前回、「何かありますか?」と聞いたらそこから1時間延長したこともありましたが、それが気になる人はアーカイブを見ておいてください。
時間もちょうどよくなりましたので、今日の勉強会はこれで終わりにしようと思います。ありがとうございました。