https://www.youtube.com/watch?v=IUOfUOHTetI
まもなく開催の今回は A Swift Tour
の「シンプルな値」から、前回に辿り着けなかった「型変換」のスライドを眺めるところから進めていきます。
前回にも口頭で話した分野でもあるので、もし時間内に型変換を見終えられるようなら、続いて「文字列補完」についても話していけるかもしれないです。よろしくお願いしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #28
00:00 開始 00:20 型変換 00:41 型は暗黙的には変換できない 01:45 イニシャライザーを通して変換する 04:29 異なる型同士での演算 07:17 JavaScriptCore 13:23 JSValue 15:10 JavaScript 環境での異なる型同士の演算 17:03 JavaScriptCore と Codable 19:41 型変換とキャスト 26:07 クラスのイニシャライザー 28:49 クラスの self と構造体の self 34:53 クラスインスタンスを差し替えることについて 37:50 型変換の表現方法 41:50 型変換であることと変換方法を明確に示す 45:22 変換イニシャライザーを搭載できない場面 50:08 コード補完との相性 51:43 次回の展望 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #28
今日は前回の続きで、型変換のところからいきたいと思います。型変換についてはAPIデザインガイドラインの中でも結構丁寧に話したと思うんですが、記憶の彼方という方も多いかと思うので、復習も兼ねていろいろとじっくり見ていこうかなと思います。
まず、型変換というのはある型のインスタンスを別の型のインスタンスに変換したいときに使います。前回の記憶が新しい方に言うのはちょっと気が引けるんですが、決して暗黙的に変換できないと書いてあります。基本原則としてSwiftでは暗黙的な変換は行いません。この点が他の言語と異なる重要なポイントです。
Swiftでは変換したいときには明示的に目的の型のインスタンスを生成します。つまり、イニシャライザーを通して変換するということです。これがSwiftの基本的な流儀になっています。
さて、この辺りをまずスライドの型変換の例を話してからプレイグラウンドで見てみましょう。一例として、ラベル
とウィズ
という変数があります。最初の変数には文字列型の値が入っていて、次の変数には数値型の値が入っています。つまり、文字列型(String
型)と数値型(Int
型)ですね。
これらを足し合わせようとした場合、Swiftでは型が違うので許されていません。どちらの型に揃えたいかを明示する必要があります。これをJavaScriptなどと比較すると、JavaScriptでは例えばラベル + ウィズ
のように書くと通りますが、Swiftでは暗黙変換が行われないため、エラーになります。
それでは実際にコードを書いてみましょう。例えば、文字列型と数値型の変数があり、それを足し合わせるとします。Swiftでは型の変換が必要です。次のように書いてみます。
let label = "Label"
let number = 1
let combinedString = label + String(number)
上記のコードでは、number
をString
型に変換してから足し合わせています。これによってエラーなく文字列を結合できます。
他の例として、JavaScriptのコードも少し紹介しますね。Swiftのプレイグラウンドが動かない場合があるので、その場合は一度リスタートしてみると良いです。JavaScriptCoreというライブラリがありますが、これは非常に便利で、iOSやMacのSDKに組み込まれているので簡単に利用できます。
次のコードでは、JavaScriptのコンテキストを作成し、スクリプトを評価する例を示します。
import JavaScriptCore
let context = JSContext()!
context.evaluateScript("var string = '1'; var number = 1; string + number;")
if let result = context.objectForKeyedSubscript("string")?.toString() {
print(result) // 1
}
このようにして、JavaScriptのコードをSwift内で実行し、結果を取得することができます。
今回の話で重要なポイントは、Swiftでは明示的な型変換が必要であり、暗黙的な変換は行われないということです。この点をしっかりと押さえておきましょう。 リザルトが取れて、このリザルトは JSValue
型になっています。オプショナル型ですね。結果が得られないこともあるので、 JSValue
型としての結果が得られます。これはいわゆる JavaScript のバリアント型です。
JavaScript では let
はローカルスコープですが、 var
になると、その関数内で使えるようになります。さらに、関数の中から別の関数を呼び出した先でも var
は使えることがあります。宣言した場所よりも遡って定義されたりする、ちょっと特殊な動作をします。 let
を使うと、Swift と違って読み書きできますが、いわゆる自然なローカルスコープになります。Swift の let
に相当するのが JavaScript の const
です。
EvaluateScript
の結果が JSValue
型で返ってきて、これは NSNumber
のように扱えるものです。例えば toNumber
を使って数値を取得すると、文字列型の変数からも数字が取れるという、JavaScript 通りの動作をします。Swift なら String
型として取れます。この辺りがとても便利で、簡単なコードで JavaScript エンジンが動いていることになります。
Swift ネイティブのクラスを JavaScript に持っていけるので、JavaScript から Swift ネイティブコードを動かして UIビューをコントロールしたり、面白いことができます。コンテキストは JavaScript と Swift で分離されていますが、内部的には統合ブリッジをしてくれますので、JavaScript のオブジェクトを Swift に持ってくることも、その逆もできます。カスタムスクリプトを提供するときにとても便利です。
JavaScript コアについてはこれくらいにしておきましょう。例えばナンバーに対して数値の 1
を入れると、コンテキストにどんどん蓄積されていくので、計算もできます。評価スクリプト (EvaluateScript
) の結果を得られるようになっています。
例えば、ストリングとナンバーを足したときにはどう動くでしょうか。実行すると 1
になりましたが、これを 1+1
として扱うためには parseInt
を使って型変換を行います。JavaScript では関数を使って型変換を行う方法がありますが、面倒だから 0
を足してあげればいいのかもしれません。しかし、この場合は文字になってしまうので、適切に処理しないといけません。
また、Number
+ String
や String
+ Number
の場合も、暗黙のルールを知っていないと正しく扱えないことがあります。そこが Swift とは違う点で、Swift では型を揃えさせられるので、明示的なコントロールを求められることになります。型を変える手段として init
や as
キーワードを使ったキャストがあります。
Codable
で JSON
との相互変換では標準ライブラリが使われていますが、JavaScript Core
は Linux にはないかもしれません。ただ、JavaScript Core
が React Native で使われているという話もあります。
アプリを配布する際にはプロビジョニングやサイニングと一緒に、JavaScript Core
のようなランタイムエンジンを使ってますよ、というチェックも必要になったりしますが、思ったより使われていないようです。それでもとても便利なものです。
Swift での型変換には initializer
が重要なポイントです。色々な型変換手段があり、as
を使ったキャストや as?
を使ったダウンキャストもあります。以上が、今回のまとめです。 上がアップで、下がダウンキャストですね。そうかそうか、ダウンキャストが下の84名の方ですね。質問にもダウンキャストって書いてあった気がしますね。上がアップキャストです。
ついついあまりこだわらずに「アップ」「ダウン」とか言っているから、いつも混乱してしまいます。ベースクラスがあって、サブクラスがあった時にサブクラスをベースクラスにするのがアップキャストですね。アップキャストは絶対に成功するというのがポイントです。
他にも、クラスの上位のやつとストリングの .NETString
みたいなものと、もう一つプロトコルがありますね。プロトコルも確かに3種類あった気がします。プロトコルも絶対成功します、適合していれば。ベースとサブがあって、あるインスタンスに対してベースとサブを持たせた場合に、サブをベースとして扱うのがアップキャストですね。
例えば、オブジェクティブCブリッジの例を考えると、Int
型の値を NSNumber
へブリッジするようなキャストがあります。各変換の方法としては、ベースクラスのイニシャライザーの引数としてサブクラスのインスタンスを渡す形を取ることが型変換です。.cgColor
みたいなプロパティを通すやつもキャストと言えばキャストですが、それは変換と呼ぶ方が適切かもしれません。
この辺りで、キャストと変換という言葉の使い方が明確ではないことがありますが、例えば Int
型に対して NSNumber
になるような変換は、確かに変換と呼べますね。
ちなみに、今ベースに対してサブを渡す変換が失敗するのは、変換用のイニシャライザーが定義されていないからです。イニシャライザーがないと変換はできません。したがって、ベースとしてサブを取るコードを書いてもエラーになります。
セルフの指定が誤っている例として、例えば構造体のセルフはミュータブルですが、クラスのセルフはイミュータブルです。クラスの self
に値を代入することはできません。
クラスに関して言えば、初期化は二段階で行う必要があります。コンビニエンスイニシャライザー(init
)か、指定イニシャライザー(designated initializer
)を呼ばないといけません。初めて見た人にとっては混乱するかもしれませんが、これがクラスと構造体の違いの一つです。
例を挙げると、セルフの値を書き換えるのはOKですが、セルフ自体を書き換えるのはNGなのです。これはクラスのインスタンスが持つポインタに由来します。
Swiftでは、クラスのインスタンスは参照型で、構造体のインスタンスは値型です。この違いは、変数のコピー方法やメモリ管理方法に影響を与えます。
以上のポイントを押さえた上で、キャストやコンバートの概念を具体的に理解すると、Swiftのプログラミングがよりスムーズに進むでしょう。 なので、これを書き換えてしまうと参照先が書き換わってしまうっていう意味合いになるんです。言葉だけで分かりますかね?ポインターって言葉を使わないでクラスの参照だと表現するのは難しいです。先生が分かりにくいですよね、結構。
クラスにおいて self
というのはインスタンスを指し示しているんですよ。ストラクトでは、self
がそのままメモリー空間を指し示しているんです。構造体のメモリーレイアウトを調べると、雰囲気が分かる人もいるかもしれません。
例えば、何らかのクラスがあったときのサイズを見ると、それはポインターのサイズにしかならないんですよね。だいたい8バイトですかね。64ビットだからポインターは。でも、ストラクトだった場合には、このサイズは定義されている通りになります。例えば、Int
が2つある構造体だと16バイトになったりします。クラスの場合、いくら Int
型のプロパティを2つ持っていたとしても、参照元はポインターのサイズでしかないんです。これが self
なんですよ。
なので、クラスの self
を書き換えてしまったとすると、元のインスタンスはどうなるのかという問題が発生してしまいます。本来であれば、リリースしないといけないわけです。この辺りが .self
に対する評価の困難さに繋がってきます。特に内部でね。ストラクトに対して self
を書き換えるというのは、例えば self
の2番目のプロパティに値を代入するようなことになります。self
に別のものを入れると、メモリ内の Int
の値が新しい Int
に置き換えられます。
構造体ではこれが許されているので、ミューテーティングなメソッドや同等なイニシャライザーの中で self
はミュータブルになります。クラスにおいては、self
はイミュータブルになり、インスタンスが勝手に置き換わるのを防ぐ形を取ります。
こういう風な違いが出てくるので、クラスの場合は自然と14行目のような対応になります。参照先のバリューを変えるっていうことですね。
じゃあ、話を戻しましょう。とりあえず、クラスに対して変換イニシャライザーを用意することによって、変換イニシャライザーが使えるようになります。こういう風な書き方が許されるようになるんです。
ちなみに、self
への代入はダメでしたよね。でも、外側からベースを書き換えることは許されています。ミューテーティングなメソッドでは self
の書き換えはできません。ここで同じようにベースを書くことはできません。ミューテーティングファンクションなんてクラスにはそもそもないんですけどね。
これができない理由は、例えばARC(Automatic Reference Counting)が破綻するからという発想もありますが、それだけではないという雰囲気がつかめればとりあえずOKです。要するに、行方不明になる危険性があるんです。ここで self
を変えたら次の self
はどうなるのか、その想像がつくと思います。さらに、ベースクラスの self
はベースだと想像しがちですが、この中で使う self
がサブクラスのこともありますし、さらにはそのサブクラスのさらにサブクラスだったりします。
こうしたときに self
が置き換わると、甚大な被害が出てくることがあります。なので、クラスの self
をイミュータブルにしている可能性もあるのです。それだけでもいろいろと考えるところがあります。
さらに、49行目と48行目の変換についても触れておきたいところです。Swiftでは両方を見ることがあると思います。多分、49行目の方をよく見かけると思いますが、48行目も根強く存在しています。それがなぜ存在するのかについてちょっと紹介します。 ちなみに、私の意見としては、できる限り49行目に揃えた方がSwiftらしいので推奨したい気持ちがあります。それでもなお48行目を許容する理由の一つとしては、これね、できないことがあるんですよ。49行目の手法が。というのも、Swiftで型変換をするときにはイニシャライザーを使うというルールがあります。イニシャライザーっていうのは、イニシャライザーが備わっている型を生成するもの、つまり変換先に対してイニシャライザーを備えなければいけないっていうことになるんです。
なので、例えば、独自にストラクトを作りました。何だろう、ハッシュタグ型みたいなのを作って、これでハッシュタグネームとしてストリングを持つ、みたいな構造体を作ったとしましょう。これを純粋にストリングに変換したいよ、みたいなことがあったとして、この例だとhashtag.name
の方が適切ではあるんですけど、ストリング型にこのハッシュタグを渡すと文字列に変換できるようにしたい、という需要があったとします。
このときにはストリング型に対してイニシャライザーを備えないといけないですね。どういうことかっていうと、エクステンションを用意してストリングに対してイニシャライザーを実装し、これでバリューとしてハッシュタグを取るというイニシャライザーを作ります。そして、self
に対してハッシュタグのネームを入れてあげることで変換イニシャライザーを実装できます。そうすると、ハッシュタグをイニシャライザーに通してストリングに変換することができるわけです。
ここで大事になってくることなんですけど、今日ちょっと話が飛びまくるので理解が難しいかもしれないですが、またアーカイブで整理してもらったり、あとで自分でも補足を入れていこうかなと思うんですけど、要は大事なところとして、変換イニシャライザーを通して変換をかける。大事なコンセプトとしてイニシャライザーを実装しなければならないっていうのが大事になってきます。
これによってどういう変換を行うべきかっていうのを、少なくとも設計者が明示的に埋め込む必要が出てくる。ここが大事なポイントです。つまり、暗黙変換というのを起こさないというものがあります。
C言語とかだと、例えば整数型(Int
)に対してある値があって、これを浮動小数点数型(Double
)を整数型に変換しようとするとき、あるダブルの値f
でいいや、こういうふうな変換をするときに、C言語、とりわけプリミティブ型というものは暗黙型変換を行います。キャストって言うんですけど、Cだと型変換をする。このときにどう変換されるかっていうのが言語に埋め込まれているんですよ。
Swiftでは変換するときにも必ずイニシャライザーを通すことによって、言語に暗黙的に組み込まれているわけではなくて、イニシャライザーに組み込まれた手法で変換をかけます。オープンソースになっているから、これがどういう変換をするかっていうのが読み取れるようになっている。細かい点かもしれないですが、こういうプリミティブなところじゃなくて、自分でハッシュタグ型みたいなのを定義したときには、ちゃんとプロジェクトの中でどういう変換方式を取るかっていうのが分かる。そしてこの書き方をすることによって、型変換をしているっていうことが分かる。
例えば、hashtag.name
っていうコードを使っても、ストリング型が得られていますが、これは別に文字列に変換していますよとは言っていないですよね。ここが結構大事なポイントで、64行目は型変換だと明らかに分かる。65行目は実質型変換になってるだけ、というところ。
これがもう少しtoString
みたいになってくると、まあ、雰囲気として分かってきますけどね。toString
関数としてtoString
を用意してストリングを返す。そしてit.name
で値を取ってあげると、さっきコメントでもらったようなJavaのtoString
みたいな関数が使えるようになってきて、まあ、68行目と69行目に並べておくと、この2つはまあ大体型変換として同等なものだねって想像がついてくるかと思います。
構造体であれば68行目ができる。構造体って変換先が構造体であれば。ただ、これがさっき作ったベース型への変換だったとき、イニシャライザーを拡張しようとするとできないんですよ。指定イニシャライザー(designated initializer
)はここでハッシュタグだ、ハッシュタグに対して。
まあちょっと何もプロパティ取ってないんで、これで変換ができたと仮定しますけど、指定イニシャライザーはエクステンションでは搭載できないので、コンビニエンスイニシャライザー(convenience initializer
)として実装しなければなりません。そしてそれで自分の別のイニシャライザーに繋いであげるという形を取れば、これで定義ができるようになって、これでベースに対してハッシュタグを渡すことができるようになりましたね。となります。
っていうふうに、できるんですよ。 ただ、これがまた難しい話になりますね。ここに書いてみますが、プロトコルの「ExpressibleByHashtag」というものを用意したとします。プロトコル「ハッシュタグで表現可能」とは、ハッシュタグ型から自身へ変換するイニシャライザーを備えようという話です。
ここで、Value
としてハッシュタグを取るということを定義します。これでベースクラスを「ExpressibleByHashtag」に適用させて、イニシャライザーを載せようとすると、問題が発生します。
プロトコルに規定されているイニシャライザーは、どんなに継承されても隠蔽されては困るので、「requiredイニシャライザー」として搭載しなければならないとされています。このルールについては、今度の勉強会でイニシャライザーの回に丁寧にお話ししますが、とりあえず「requiredイニシャライザー」を搭載しないといけないと思ってください。
ただ、convenience required
イニシャライザーを搭載しようとしても、requiredイニシャライザー
はクラス内やエクステンション内では実装ができないんですよね。したがって、ベースクラスのエクステンションではなく、実装の方に搭載しなければなりません。ここに搭載さえされていれば、エクステンションではなく実装として持てるため、コンパイルが通るようになります。
自分で作ったベースクラスだから内部に持てましたが、他の人が作ったクラスに対しては内部に持たせることができず、エクステンションを使わざるを得ないのです。つまり、イニシャライザーとして変換イニシャライザーを持たせることができなくなり、メソッドを通じた変換を備えなければならなくなってくるわけです。
そうした理由から、クラスを見越したときには、どうしてもイニシャライザーを実装しなければならない状況になります。また、コメントでももらった「補完が効くから便利」というのは確かにあります。手戻り感もありますよね。Swiftではこちらの方法を推奨していますが、ハッシュタグを文字列として使いたいと思った瞬間に手戻りを書く必要が生じます。hashtag.toString
の方が書きやすいのは間違いないですが、APIデザインガイドラインに反するので悩ましい所です。
ハッシュタグを書き終わった後に変換できると便利だとは思いますが、メソッドチェーンする場合に括弧を付けなければならなかったりします。この書き方は正式な書き方ではなくエラーですが、こんな感じです。
とにかく正式な方法としては、文字列変換ならイニシャライザーを通すという話でした。型変換についてはこれで一通り話せたかなと思います。
時間にもなったので、今日は型変換の話とJavaScriptCoreの話で終わります。これで今日は終了です。また来週月曜日に続き、次回は「Stringインターポレーション」について話します。型の文字列補完はなかなか面白いところなので、じっくり見ていこうと思います。
はい、時間になりましたので、これで終わりです。お疲れ様でした。