https://www.youtube.com/watch?v=rU7ep_3KeEM
10 日ぶりの今回は A Swift Tour
の「シンプルな値」の続きを眺めていきます。そのうちの変数や定数の宣言と再代入については前回に見ていきましたので、今回はその続き、型推論と型変換あたりの雰囲気を見ていくことになりそうです。どうぞよろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #26
00:00:00 開始 00:01:27 型推論 00:02:46 静的型付け 00:05:03 同じ型同士で演算を行う 00:06:50 型推論の例 00:11:13 リテラルの既定の型 00:21:15 イニシャライザーの推論 00:22:50 リテラルの既定の型を変更する 00:28:07 リテラルがインスタンス化される仕組み 00:37:03 予約語と名前空間 00:41:43 スコープとシャドーイング 00:44:54 言語の組み込み機能の扱い 00:49:36 リテラルの既定の型の適用のされ方 00:51:38 Bool を 1 で表現することについて 00:59:17 Objective-C Bridge 01:00:49 Reference Convertible 01:12:35 Swift の型変換の流儀 01:16:15 既定の型のアクセス範囲 01:20:49 Double と CGFloat の相互変換 01:26:17 CGFloat が特別扱いされる? 01:36:20 CGFloat と Double の暗黙変換 01:38:16 Swift が言うのなら正しい、という価値観 01:44:37 次回の展望 ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #26
前回、「ハローワールド」についての話が終わりましたね。その際、いろいろとお話しして、その次に「シンプルバリュー」として整数と変数の宣言と初期化についても触れました。10日前なので少し忘れている方もいるかと思いますが、あえてキーワードだけおさらいすると、Swiftには「Definite Initialization」という初期化を先送りできる仕組みがあります。これを理解する上で大事になるのが、変数の宣言のフェーズです。変数の準備のフェーズ、宣言、初期化、最大入参照などをしっかり意識すると、Swiftの視野がとても広がります。ということで、これらを意識することを心がけると、少し理解が深まるかもしれません、という話を前回しました。
さて、次に進んで今回は「型推論」について話していきます。Swiftのツアーでは、この型推論の話はさらっと紹介されていますが、言葉から感じる重々しい雰囲気とは裏腹に、実際には大したことをしているわけではありません。文脈や前後関係から、その変数や定数の型が推察できれば、その型に自動的に決まるというものです。
具体的には、前後関係から「これは文字列だろう」、「これは数値だろう」といった具合に、型が自動的に決まる仕組みです。数値の中でも Int32
だろうとか、Int64
だろうとか、そういう風にして型が決まっていきます。型推論があるおかげで、型を意識しなくても自然にプログラムが組み立てられていき、最終的には理想の型に持っていくことができます。もっとも、これは何も意識しなくても最適な解が得られるという話です。
例えば、以下のように書くと:
let a = 10
この場合、定数 a
に 10
を代入しています。これだけで a
の型が自動的に Int
型と決まります。これは、全体のステートメントを見たときに代入しようとしている右辺の値が 10
であり、この 10
が整数リテラルであることから、特に型が明記されていなければ Int
型と見なされるからです。
これが型推論の基本的な内容です。これにより、プログラマーがいちいち型を書かなくてもよくなり、コーディングがかなり楽になります。
しかしながら、型に厳格な言語とは少し違います。例えば、他のプログラミング言語、たとえば Perl や JavaScript、PHPのような動的型付け言語とは違い、Swiftでは「この変数はこの型である」とコンパイル時に型が決まるため、より厳密な型チェックが行われます。
さらに、Swiftでは基本的に2つ以上の値を扱う場合は同じ型で揃える必要があります。例えば、以下のように書くと:
let a: Int8 = 10
この場合は、a
が Int8
型で定義されているため、代入しようとしている整数リテラル 10
も自動的に Int8
型と見なされます。
こういった型推論によって、Swiftは安全かつ効率的にプログラミングを進めることができます。この話を理解しておけば、少なくとも型推論に関する基本的な理解は得られるでしょう。 そうすると、整数リテラル10はInt8
型に変換して使おうというふうに自動的に解釈されて、こういう動きを見せてくれます。これも多分型推論とひっくるめて問題ないはずです。型推論がどこで効果を発揮するか、どこが不明瞭かによって推論される内容が変わってきます。
もしPrologという言語を使ったことがある人がいたら、変数のマッチングのように、空いている部分を補完するという感覚に近いです。
とても良いコメントが寄せられました。リテラルのデフォルト型はコード上のどこかに見事に定義されているんですよ、これが面白いところです。どこから説明すると面白いかな。私は初めてそれを見つけたのがリテラル変換に対応させたときでした。その時、自分で定義した型を見つけました。その追体験のような感じになるかもしれません。
例えば、シリアルナンバーという型を作ったとします。これでシリアルにしよう、やっぱり。これで「ナンバー」を持っているような型を作って使おうとします。シリアルナンバーとしてオブジェクトを持たせます。これが階層の深い例になるので分かりにくかったら無視してください。
次にシリアルナンバーの初期化を行います。例えば、ナンバー1とします。これがコードとして許可されます。しかし、シリアルナンバーは通常、シリアルナンバーを取るのでラベル名を省略してナンバーを入れると良いですね。そうすると、シリアルナンバーのイニシャライザーが不要になります。
シリアルナンバーは普通、整数ですね。そうではない場合もありますが、一般的には1から始まる通し番号です。ここでは整数型で扱うとします。このように設計して、このシリアルナンバー型を整数リテラルに対応させることができます。
そのためにはExpressibleByIntegerLiteral
というプロトコルを使います。これに適用させる必要がありますが、どんな要求があるかというと、付属型としてIntegerLiteralType
を取り、それを使って初期化するイニシャライザーを実装する必要があります。
このIntegerLiteralType
が何型なのか決まっていないので、自分の好きなInt
型に受け取って初期化します。昔これを初めて見たとき、IntegerLiteralType
とは何か疑問に思いました。
フリーなスペースでIntegerLiteralType
と打ってみると、コード補完に出てきます。これを見ると、typealias IntegerLiteralType = Int
と書いてあります。つまり、IntegerLiteralType
はInt
型なのです。コメントには、IntegerLiteralType
の規定値と書いてあります。要するに、規定値がInt
ということです。
では実装してみましょう。型としてInt
型を採用し、シリアルナンバーのイニシャライザーにその値を受け取るようにします。これでコンパイルが通ります。
ここで定数リテラルを見てみます。まず、型を明記していない変数プレートの宣言があり、右辺のメモリアルプレートのイニシャライズ処理があります。この場合、変数プレートは自動的にメモリアルプレート型と推論されます。そして、整数リテラル1は通常Int
型ですが、ここではメモリアルプレートのシリアルナンバーとして渡されるので、シリアルナンバー型として処理されます。 なので、シリアルナンバーラベルの引数に取るものはシリアルナンバー型のインスタンスを取ります。そうすると、この整数リテラル1は Int
型ではなくシリアルナンバー型であるはずだということをまず推定します。そうすると、このシリアルナンバー型がリテラルに対応しているのかを判断します。ここでシリアルナンバー型は ExpressibleByIntegerLiteral
に準拠しているので、整数リテラルから変換可能です。つまり、整数リテラル1はシリアルナンバー型として扱えます。このような形で型推論が進んでいくわけです。
型推論はどんどん積み重なっていって、非常に表現力が高まっていきます。例えば、pleate1
と pleate2
を並べて書いてみますが、どちらも同じことを書いているわけです。しかし、型推論の発想がなかった時のプログラミング言語では、22行目のような書き方をせざるを得なかったけれど、型推論があると、23行目のような書き方ができるようになります。多くの人が、ほとんどの場合、23行目の方がわかりやすいと感じると思います。
まず、型推論とはこういったもので、さらに高度なものがあるわけです。メモリアルプレートの書き方は、イニシャライザーを呼んでいるというある意味省略形です。省略形とは言われてないですけどね。このような形になるわけです。
メソッドの呼び出し、とりわけ型メソッド(いわゆるスタティックメソッドのようなもの)になると、どの型に所属しているものかという推論もできるようになっています。例えば、pleate2
がメモリアルプレート型と宣言されていると、その右辺で使われるイニシャライザーは明記されなくても普通メモリアルプレート型だろうと察してくれて、これでもちゃんと動きます。このように型名を明記するかどうかという、もう少し上層の部分に対しても、型推論が効いてくるのは面白いところです。
Genericsと関係すると非常に高度な動きを見せてきて面白いので、この後ちょっと紹介しようかと思います。まずは、Zoomのコメントで投げかけられた疑問を回収しますけど、デフォルトのリテラルの変換ですね。さっき見た IntegerLiteralType
って何だろうって思うかもしれませんが、その定義が typealias
でしたよというお話をしました。つまり、1と書くとこれは Int
型なわけです。
type(of: 1)
と書くと Int
型になります。これは全体を通してどこにも型が明記されていないときに、関数 type(of:)
がどんな型でも取ってメタタイプを返す関数になっているので、全体で見ても何も明記されてないので、1
を IntegerLiteralType
で定義されている Int
型として解釈します。つまり、Int
型のインスタンスが生成されて、そのインスタンスの型を見てみると Int
型だということがわかります。
ここで、規定値(デフォルト)の型が大事なポイントです。プログラミングにおいて、規定値というものは別のものに置き換えられるわけです。typealias
として IntegerLiteralType
をシリアルナンバーに置き換えてあげると、整数リテラルが自動的にシリアルナンバー型として解釈されるようになります。他にも、例えば Double
型を規定値にすると、整数リテラル 1
が Double
型に変わります。この動作がとても大事です。
あくまでも型推論においての規定値ということなので、何も制約がない場合には今回の type(of:)
によると Int
型になります。ただし、実行環境によっては動作が変わることもありますので、自分の環境でも試してみてください。再実行ボタンがあるか確認してみてください。 ここにコードを書き換える必要があることがあります。これは少し怖いですよね。しかし、コンパイル環境では大丈夫なはずです。プレイグラウンドで試してみてください。素晴らしいですね。
例えば、「レッド K イコール 1」といったコードを見た時に、データ型が決定されないことがあります。右辺のリテラルだけでは型が推定されないため、推論を行います。Swift では、整数リテラルが与えられたときに Expressible by Integer Literal
に準拠させる必要があります。このプロトコルに準拠していれば、リテラルの変換が可能となります。
let k = 1
この 1 行のコードだけでは、型が何であるかは明確ではありません。しかし、整数リテラルの場合、明確な制約がなければ Integer Literal Type
として評価されるという Swift の仕様があります。この仕様に基づいて型推論が行われ、例えば let
の右辺が Int
型として推定されます。
さらに、整数リテラルから変換可能な型には様々なものがあります。 Expressible by Integer Literal
プロトコルによるイニシャライザを定義し、その中で特定の型に変換することができます。
struct MyNumber: ExpressibleByIntegerLiteral {
let value: Int
init(integerLiteral value: Int) {
self.value = value
}
}
let num: MyNumber = 42
ここで重要なのは、リテラルが与えられた時に適用される型推論の順序です。例えば、let l
が何も規定されていない場合、デフォルトの型として Double
になる可能性があります。同時に、整数リテラルから変換可能な他の型も同様に推論されます。
結局のところ、Swift ではリテラルに対して適用されるプロトコルを定義し、そのプロトコルに基づいた型推論と変換が行われます。リテラルの解釈と型推定の仕組みを理解することが、正しい型付けを行う上で非常に重要です。 なので、ここで最終的なものを見つけていって、右辺では Int
型としては定義されていない、明記されていないけど、一番絶対的な力を持っている型は Int
型なので、じゃあこれは Int
型でしょう、IntegerLiteral
タイプでしょう、ではなくて Int
型でしょう、という風に解釈されます。そうすると、いよいよ Int
型に変換がかけられて、Int
の IntegerLiteral 1
という風な解釈で動いていきます。同じ整数リテラル 1
って書いたけれど問題なく、あくまでもデフォルト、デフォルト以外でそれより優先度の高い型推論的に優先度がある、って感じですかね。だから型推論的に見てこっちの方が最適だとなると、規定の型を使わないで特別な型を選んでいくということになります。
なかなか良い質問がさらに来ていますが、typealias
で定義されている型が予約語として引っかからないかというところは何も心配がありません。予約語ではないですね、typealias
で決めてあるものは。そして、大事なポイントとしては名前空間を意識する必要があります。例えば、typealias A = Int
として、これでまた同じ名前空間の中で typealias A = W
とやると、これはエラーになります。大事なポイントとしては、再代入された、再定義されたというエラーになるので、どっちを取っていいか分からない状態ができるとエラーになります。逆に、どっちを取ればいいかが分かる状況であればエラーにならなくて、例えばクラスで class Object
とあって、この中で typealias A = W
っていう風に書くと、これはコンパイルが通ります、問題なく。
どういう風な解釈かというと、あるファンクションがあったときにオブジェクトの中で A
を使おうとすると W
型、これは自分の名前空間の中の typealias A
を優先して使うので W
型。でもこの外で function something()
、そして A
を使おうとすると Int
型。ここを見ていますからね、これは自分の名前空間の中で A
を基準に見たときに A = Int
型ってなってるからです。これをもしね、オブジェクトの中の A
として使いたいときには、名前空間を明記して使うと W
型になるみたいな。こういう風に名前空間として分離されているので、このオブジェクトクラスの中から見ると、typealias A
が上書きされたみたいなイメージです。上書きではないんですけど、こういう風になっているっていうのがまず大事なポイントです。
さらに言うと、モジュールも名前空間を構成します。このクラスでオブジェクトが今名前空間を構成しているのと同じように、import Swift
とかモジュールをインポートしたときには、このモジュール自体も名前空間を作ります。そうすると、Swiftの中にある IntegerLiteralType
っていうのと、あと現在のモジュールにある、あるっていうか、自分で定義したのであるんですけど、IntegerLiteralType
のこの2つが存在する、っていう風になってて、さらにここに優先順位っていうものがあります。自分のモジュールにインポートしたモジュールは優先度が1つレベル低いんですよ。なので何もなければ下のモジュールの、同じ名前がなければ下のモジュールから引っ張ってきますけど、同じモジュールにその目的の名前があったら、その下にあるものは引っ張ってこない、っていうような感じですかね。ちょっと難しいですが。
スコープの考え方と一緒ですね。だからその大事なポイントとしては、予約語とか再定義じゃないんですよ。隠蔽する、シャドーイングっていうのかな。どっちかっていうと、シャドーイングの主だった例としては分かりやすいのがフォーループかな。例えば、分かりやすいも何もないか、if let
だね。例えば何かしらの値が Int?
型であって、ファンクションの方がいいかもね。ファンクションで何かをする、ここでパラメータとしてオプションを取る。オプションは Int
型で取るけど、オプションを取らないこともあるよ、みたいなね、コードを書いたとしますよね。= nil
と書いた方が意味が通じやすいかな。オプションは Int
型のフラグなんだけど、何も渡さないこともありますよ。でもフラグが渡ってきた時には何か処理をします。 普通、コードを書くときに「オプション=オプション」という書き方をすることがあります。この書き方は、Swiftをやっている人にはおなじみだと思います。もともとあったInt型の変数を、オプショナルなInt型の変数としてオプショナルバインディングでさらに同じ名前で定義し、値があった時にはそれをInt型として使います。
同じように、タイプエイリアスも上位のモジュールにあるパラメータと、たまたま同じ名前のパラメータを現在のモジュールに定義しておくと、その名前を使った時には、一番直近のモジュールのものが採用される動き方をします。タイプエイリアスやInt
リテラルタイプも同じです。
Swiftモジュールに定義されているものは、予約語と勘違いされることがありますが、Swiftモジュールの中にあるものは、皆がコードを書く時のルールとほぼ同じルールに基づいて書かれているので、特別な存在ではありません。名前空間より自分に近い側の名前空間のものに置き換えることが普通にできます。
しかし、Swiftのモジュールよりも下、つまりコンパイラーに組み込まれているものになると、自分の意志ではどうにもならなくなります。例えば、何も制約がない状況でのリテラル変換がInt
リテラルタイプとみなされる部分などは、コンパイラーに組み込まれていてユーザー側で変更はできません。
また、変数が例えばM
というInt型で、それを別の変数に代入する場合、Int型は構造体、つまり値型として扱われるので代入時には絶対に複製が行われ、N
に保存される値は別物になります。しかし、これがNSNumberというクラス型の変数であった場合、参照型となるためN
にはM
のリファレンスが代入されます。このような動きをカスタマイズすることもできません。Swiftモジュールではなく、さらにその最下層のコンパイラーに組み込まれているからです。
この=
演算子やas
の動作をカスタマイズする術は用意されていません。このような基本的な動作は、Swiftモジュールにあるものではないため、遺伝子的なものに近いといえます。
インポートSwiftの中にあるかそもそもないかによって、カスタマイズする余地があるか否かが判断できます。この見方でSwift標準ライブラリを見ていくのも面白いかもしれません。
最後に、1分ほどお話を受け付けますが、特にご質問がなければこれで終わりにします。デフォルトリテラルタイプをIntリテラルタイプとして決めるコンパイラーの動きについてですが、デフォルトリテラルタイプは重複しないで型を決めています。
では、今日の勉強会はこれで終了とさせていただきます。お疲れ様でした。 どうもありがとうございました。
例えば、C言語とかではBooleanを 1
などで書くことができるじゃないですか。
はい、書けますね。
ですが、Swiftではそれができないと思います。その理由としては、全てが繋がっているというか、もし 1
がBooleanになり得るとすると、Booleanリテラルタイプも適当にできることになりますよね。
そうですね。しかし、多分しないので、 1
と書かれたらインテジャリテラルタイプしかあり得ないという言語構造になっているのかもしれません。
必ずしもそうとは言えないかもしれませんが、これを見る視点によって疑問の解釈が変わるかもしれません。私の解釈がまだ及んでいないだけかもしれませんが、今お話ししてくれた感じだと、見る視点によってイメージが違ってくる気がします。
例えば、エクステンションでBoolean型に対して ExpressibleByIntegerLiteral
を実装して、インテジャリテラル型として値を受け取る。そこで self = (value == 0) ? false : true
のようにしてあげると、Int型がBooleanになり得ますよね。
なるほど。ではやはり、デフォルトリテラルタイプを決める前にどの型になり得るのか、コンパイラは認識しないとデフォルトリテラルタイプを選べないということですかね。
多分そうだと思います。型推論をまずパーサーが型を持たない状態で一回パースし、その後に型を決めるフェーズに入ります。その際、インテジャリテラルがどの型に当たるかを見終わった後に、どっちの型かを決めて確定するという流れになります。
ルールとかダブル型ってデフォルトで ExpressibleByIntegerLiteral
プロトコルに準拠していますよね。
そうですね。でも、個別の実装を見ると、 0
や 1
のような値でBooleanに変換することも可能です。逆にそうしないと不具合が起こるかもしれないですね。
今動かないのは、もしかして別のライブラリかフレームワークが影響しているかもしれません。アップキャストの仕組みについても理解がまだ浅いですが、これは型推論ではなく型変換ですね。
1 as Int
という形式で指定すると、型変換していることになりますね。
確かに、アズによる型推論と型変換は異なる場合があります。 1
という値は型推論されてインテジャと見なされ、 as
を使うとその型に変換されるという仕組みですね。
NSNumBerに関連する部分は特に特殊な取り扱いが必要で、可読性を保つためには型が決まっている状態で as
を使う場合にも注意が必要です。
Swiftの標準ルールでは、型が決まっている状態で as
を使う場合、必ずビックリマークやハテナを使って明示的にキャストを行うことが求められています。この点を理解しておくことが重要です。 なので、NSNumber
じゃなくて、Double
やInt
型のような種類型に変換したい場合にas
を直接使うことはできず、コンパイルエラーになります。そのため、?
や!
が必要となりますが、それでも動的に変換できない場合、nil
が返ってきます。こういった動作を理解していないと、型が違うことによるエラーを見逃してしまいます。
たとえば、Int
型やDouble
型について話します。それらもSwiftで同じ発想で作られています。Int
型とDouble
型があり、それぞれに例えばlet i: Int = 1
のように値を持つことができます。そして、i as NSNumber
といった変換が可能です。これがもしもカスタムの値型だった場合、同じようにはいかないのですが、extension
を使ってValue
に対してReferenceConvertible
を提供することで、コンパイラが指示してくれます。
ReferenceConvertible
を使う場合、ObjectiveCBridgeable
に準拠している必要があります。これは具体的にどの参照型と互換性があるのかを明記するプロトコルです。たとえば、SwiftのInt
型の場合、それに対応する参照型としてNSNumber
が使われます。
したがって、as
を使ってInt
型の値をNSNumber
に変換できるのは、Foundation
フレームワークの中でこのReferenceConvertible
プロトコルが実装されているためです。
具体的に、以下のような感じで実装します:
extension Int: ReferenceConvertible {
typealias ReferenceType = NSNumber
// その他のプロトコル要件を満たす実装が追加されます
}
このようにして、Swiftの値型がNSNumber
との互換性を持つようになります。必要となる他のメソッドもいくつか実装が必要です。具体的には、isObjectiveCBridgeable
のようなメソッドを実装し、変換が可能かどうかを判定したり、bridgeToObjectiveC
メソッドを使って具体的にNSNumber
に変換する処理を行います。
extension Int: ReferenceConvertible {
typealias ReferenceType = NSNumber
static func isObjectiveCBridgeable() -> Bool {
return true
}
func bridgeToObjectiveC() -> NSNumber {
return NSNumber(value: self)
}
static func forceBridgeFromObjectiveC(_ source: NSNumber, result: inout Int?) -> Bool {
result = source.intValue
return true
}
// 他にも必要なメソッドを実装します
}
こうすることで、Swiftの型とObjective-Cの型の間で自然な変換が可能となり、!
や?
を使った問答が不要になります。この整備された型変換により、より安全で直感的なコードを書くことができるようになります。 ASの話ですが、若干特殊な部分があります。通常のas
やas?
、as!
のキャストとは異なり、この場合、新しいインスタンスを生成するものになります。この点は非常に注意が必要です。通常のキャストは型変換ですが、この場合はインスタンス自体も変換することになります。新しいインスタンスを生成し、イニシャライザーが差し込まれる形になります。
これを実装することは可能ですが、自動的にFix-It
で補完されることはなく、隠されているために手動で実装する必要があります。具体的には、SwiftのオープンソースのGitHubリポジトリにあるUnderscore Objective-C Bridgeable
の定義を見て必要なメソッドを確認し、それらを実装しないとコンパイルエラーになります。このようにすべてのメソッドを実装しない限り、as
が使えない状況になります。
AppleのSwift言語設計者によると、個人的に楽しむ分には問題ありませんが、将来的に挙動が変わる可能性があるので注意が必要です。つまりUnderscore
で定義されたものは、策定中やワークアラウンドとして存在する不安定な部分であり、バージョンアップ時に動かなくなる可能性があります。そのため、プロジェクトでの使用は自己責任となります。
趣味の範囲で使うのは問題ありませんが、実際のプロダクトで使用する場合は、将来的なバージョンアップに対応できる準備が必要です。特に、独自のイニシャライザーを用意すれば、対応はそれほど困難ではありません。as
を使うことは確かにスタイリッシュに感じますが、標準的な型変換の手法を使う方が無難です。
暗黙のイニシャライザーは見えないため、知らない人には説明が求められることがあります。例えば、イニシャライザーと何が異なるのかと聞かれた時、しっかり説明できるのであれば問題ありませんが、やはり使う範囲は限定した方が良いでしょう。初心者や中級レベルのプログラマーにとって分かりやすい書き方ですので、自分の手の届く範囲のエリアで使う分には構わないですが、ライブラリーとして公開するのは避けた方が良いかと思います。 面白いですよね。さらっと書いている部分にも、Bridgable
が関わっていたりして、興味深いですぜひ個人的にはやってもらいたいです。面白いので、これが組み込まれていないというのがSwiftっぽいなと思いますね。
僕は最低限のところは組み込みますが、その上側は標準ライブラリーに入れていく。この前話したインテジャーリテラルタイプも、触れるところに置いておくのはすごいなと思います。
確かにそうですね。通常はInt
固定というのが原則のルールに置かれたりしますが、Swiftではその仕様を外に出しているのです。これはtypealias
なんで、今のスコープで変えることができる柔軟性を確保しています。
良いですね。例えば、ファイルがあって、この中でpublic
として定義しても、外まで暗黙のデフォルトにはしないんです。しかし、逆にプライベートでtypealias
として例えばFloatLiteralType
を定義して、不動小数点数リテラルの挙動を変えることができます。これで例えば、このファイルでCoreGraphicsを書く場合、毎回CGFloat
を頑張って書く人がいると思います。
この時に、CGFloat
と書いておくと、それが自動的に変換されます。とても便利ですよね。だからプライベートで変換するというのは、この知識が行き届かないとみんなに分かりませんが、この書き方をすればCGFloat
になるというのは、非常に読みやすいコードになります。
冒頭で「ここではFloatLiteralType
はCGFloat
としますよ」という宣言を書いておき、その後は普通に書き進めていく。変数定義と何も変わらないですね。「let pi: CGFloat = 3.14」とするのと同じ重みです。だから、このプライベートtypealias
を理解できた人は、ぜひ使ってSwiftの技術レベル向上に貢献して欲しいと思います。
レビューで心配される方もいらっしゃるかもしれませんが、そんなに誤解を招く仕様ではないと思います。ただ、残念ながら関数内では機能しないんです。ファイルのトップレベルでは動作しますが、クラスやストラクトの中でも機能しません。
ファイルレベルのトップで使えるので、プライベートな形で使うと良いでしょう。でも、CGFloat
が頻繁に出てくる場合には確かに役立ちます。型の互換性が問われたときも、変数がDouble
型だった場合はエラーになります。
例えばCoreGraphicsで動作させる場合でも、複雑なコードでは見通しが悪くなりがちですが、このレベルでもちゃんと変更できます。短いコードでリテラルタイプも扱えるので便利です。
ただ、プレイグラウンドでは動作しない場合もあり、Double
にキャストされてしまう時もあります。なぜだかわかりませんが、CGFloat
は暗黙的に変換されませんよね。それについては調査が必要です。 Float
からDouble
に変換できちゃう。これはPlaygroundですね。変換できるのであれば、わざわざ買わなくてもいいですよ。64ビットだとFloat
は常にDouble
ですよね。タイプエイリアスを使っていますか?パブリックストラクターですね。そうか、昔はタイプエイリアスだったけど、途中から変えたんですね。これ、なんで変換できるのか自分でもわかりません。ちょっと違和感がありますが、動くなら問題ないです。動かなかったときは不便ですが、動けば全然問題ないですね。
Objective-C
のコードを書いているときにも、as
のキャストが必要になりますね。この部分、動きが全然違うので注意が必要です。これはDouble
のイニシャライズを読んでいるのでしょうか?そんな勝手なことはしないと思いますが、非常に謎です。自分でもまだ把握できていないので、教えられるところではありませんが、とりあえず今回紹介したかったのはここまでです。
フロートリテラルタイプがあるかどうかで、リテラルがFloat
になるかDouble
になるかという動きが変わります。他の環境、例えばObjective-C
環境のプログラムを書いている場合、NSNumber
じゃないと困ることがあります。そのときは、全部NSNumber
に寄せることもできます。ただ、変換に心配があるので、あまりやらないほうがいいかもしれません。型が違うので普通はそうなりますよね。Double
の値を取得するわけですから。
言いたかったのはこれです。本来、型が違えばコンパイルエラーになります。仮にフロートリテラルを別の型に解釈するようにしても、見つかりますよね。だから、Double
型よりFloat
のほうが圧倒的に利用頻度が高い場合は、Float
を規定の型として優先し、特殊な場面だけDouble
に変換するのがAPIデザインガイドライン的にも冗長でなく良いのではないかという気がします。
コメントにもありますが、ネイティブ型が気になりますね。Float
のネイティブタイプがどう処理されているかは個人的には暗黙的な処理を考えにくいです。特にFoundation
やCoreGraphics
の機能は、標準ライブラリには入っていないので、特別扱いされるとは考えにくいです。何かしらのプロトコルで説明されているのではないかというのが常識的な発想です。
Float
とDouble
の違いについては、Double
の方を見るべきかもしれません。Double
がFloat
を受け付けている可能性もありますね。Objective-C
の場合、元の方で定義したりするので断言はできませんが、確かにその発想は理にかなっています。
エクステンションでどこで入るかわからないというのが辛いところです。CoreGraphics
のFloat
としてコンパイルが成り立つのは、一体いつからなのでしょうか。これは非常に重要なポイントですね。 これでグラフィックスのベース、つまりCGベースですね。これで通るってことは、この中の可能性がとても高いですよね。辿れた定義として、CGベースを辿れてます。これでほとんどコメントは辿れてますね。CGフロート、もう多分C言語レベルの実装になってて、表には少ししか出てきてない気がしますね。
CGフロートミンとCGフロートマックスがダブルで返ってくるっていう、非常に興味深いコードが書かれてますね。これがCGフロート型で返すことでしょう。普通そうですよね。それ以上の詳細は分からないですけど、表向きにはストラクトとしてCGフロートは存在しますが、内側では同じものと見ている可能性がないとは言えないです。だから表向きには一緒なのを、さらにユーザー層向けの便宜上をCGフロート、要はタイプエイリアス的な表現があって、それを使って実質同じものに見えている感じですね。コンパイラーレベルでは一緒なのでコンパイルが通っていると考えられます。
ここで疑問が湧いてきますね。CGフロートに自分で独自のものを定義したときに、両方に入るのかどうかっていうことです。こうやって実験を重ねるのがプログラミングの楽しみですよね。
これでAがダブル型だから、「マイプロパティ」なんてものは存在しないとエラーになるはずです。見てみましょう。マイプロパティでエラーが出てますね。「ノーメンバーマイプロパティ」。でも、パイとして定義したものにはありそうですね。ボイドが返ってくる、何も出力されないけど、ボイド返してるのでね。ということは、Swiftの上側的、ユーザー層側的には型を区別してますね、ダブル型とCGフロート型を。でも、代入はできちゃってるというのは、もしかするとLLVMとか低レベル側のところでうまくいっちゃってる感じが強いです。自分の評価では、たまたまうまくいっちゃってる感じが強くて、いずれここで型が違うよっていう適切なエラーが出るんじゃないかという雰囲気を感じます。
チャットに送ったデータでは、代入できるんですね。CGフロート。こういう例、大事ですよね。代入できるのは普通のプロジェクトで確認すると、コンパイル通るので、CGフロートのストラクトとダブルストラクト、そういう一連の判別が必要ですね。27行目、28行目、変数名が重複しちゃってる。AとAにしちゃった、これおかしいですね。
試してみましたが、CGフロート型はSwiftの標準ライブラリレベルで違う型として認識しています。同じ型として認識した場合、CGフロートへの拡張がダブル型に反映されるはずです。おそらくこれはコアグラフィックスのインポートがCライブラリのモジュールをSwiftに再インポートしているためだと思われます。
例えば、インポート、インポート、SQLiteとかが標準でありますけど、これと同じノリのはずなんです。コアグラフィックスって、SQLiteのライブラリがCで提供されているはずです。Cインターオペラビリティって明示的にはわからないですが、Swiftにいい具合に揃えていこうという動きがあります。
また、Swift 5.5の動きがあり、新しい価値観かもしれませんが、オートマティックジャンパーなんて機能も追加されています。これはSwiftの老害的な発想になりますけど、個人的にはあまり賛成できない気がします。しかし、面倒だからという意識もあったのかもしれません。
結局、Swift UIで面倒くさいからという理由で導入されたかもしれませんが、個人的にはこの方法をお勧めします。タイプコンバージョンを導入するより、もっと読みやすくするべきです。このようにエクステンションがばらばらだと、秩序が乱れます。
最後に、Objective-C Bridgeableの時点で混乱しがちですが、これはSwiftのネイティブタイプと少し似ています。 Swift信者の大事なポイントとして、「Swiftがそう言ったなら正しいんだろう」という発想も大事なんですよね。昔、Swiftがバージョン1として登場した時に、Objective-Cと比較して「これ、ありえなくない?」と思った人がいっぱいいたと思います。あの頃からやっている人たちの中には「でも、自分の経験上、Swiftが言ってるんだから正しいだろう」と意識するようになって、「なるほど、確かに素晴らしかった」と受け入れるようになった人もいるかもしれません。洗脳されているのか、よくわからないんですが、そういう風になるんでね。これは正しいのかもしれない、と。
ただ、今のところやりすぎ感を感じることもありますが、こういった仕様はSwiftUIでどれだけ短く書けるかに影響している気がしますね。とはいえ、本来はそれをやってはいけないと思います。原稿仕様を覚えることが多くなってしまうからです。Swiftを覚える人にとっては、結構ハードルが高いですよね。本当に。何気なく使う分には使えるんですが、技術者的な視点で深く理解しようとすると、かなり重たいです。
さて、これ何をやっていたんだろう?ちょっと見てみましょう。「なぜコンバートできるのか」というところが自分は知りたいので、詳しく見ていこうと思います。解決策もタイプエリアで局所的に制限を抑え込むことで、SwiftUIのインポートのついでに書けばいいわけです。それこそおまじない的に、「SwiftUIだからタイプエリア数もCGFloat
にしとこう」みたいに書けば、何の言語拡張もいらずにできてしまうのに、なぜこの変換を取り入れたのか理解に苦しみます。
さっきの内容に関連していますが、Double
型を受け取る場合とCGFloat
を受け取る場合があるから型に書きたい、という話がありましたね。でも、それだと型ミキシングすることになります。例えば、Double
を受け取る場合には let pi: Double = 3.14
と書けば、Double
型になりますよね。なので、型のミキシングはあまり関係ないですね。
確かに型が一度決まってしまうと、相互変換がつらくなります。コメントにも寄せてもらっているように、Double
とCGFloat
の相互変換ができないから、これが問題として浮上してくるんですね。SwiftUIにDouble
を取るものとCGFloat
を取るものがあるから入った、というのは納得します。
でも、毎回Double
と書けというのがオーソドックスなSwiftのルールですが、それでもSwiftUI側で直してほしいなと感じますね。SwiftUIがCGFloat
を扱わないようにして、全部Double
にしてしまえばいいのですから。
また、今のところSwiftUIはコアライブラリに入っていないですが、将来的に互換性を考慮するとコアライブラリに入ってくると思います。その時、コアファンデーションやコアグラフィックスのCGFloat
は標準ライブラリやコアライブラリには入ってこないはずです。SwiftUIのフロートはCGFloat
ではなくDouble
型に統一される状況になった時に、今回のCGFloat
とDouble
型の互換性を暗黙型変換という形でクッションする仕様は無駄になります。
このように、将来にわたって問題が継続する仕様は好ましくないと思います。後々対応することになるわけですからね。
すみませんが、次の予定があるので、これくらいで終わりにしましょう。本日は中途半端な問題を取り上げてしまいましたが、大勢の方がお付き合いくださり、ありがとうございました。次回は型変換についての話を続けて取り上げますので、ぜひ興味があればご参加ください。2時間にわたってお疲れ様でした。ご視聴ありがとうございました。