https://youtu.be/KnFZXBmGz3Y
本日は、これまでに見てきた The Basics
の 定数と変数の出力
を踏まえて、インスタンスを文字列化することについてを具体的に眺めていきます。普段は何気なく使いがちな CustomStringConvertible
で、そんな感じの使い方でもじゅうぶん活用できるところはありますけれど、改めて意識的な扱い方について確認できたら今後のコーディングに何か活きてくるかもしれないです。どうぞよろしくお願いしますね。
—————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #104
00:00 開始 01:01 これまでのあらすじ 02:27 CustomStringConvertible 03:37 構造体の既定のテキスト表現 05:23 インスタンスが表現する値をテキストで表現 07:15 テキスト表現からインスタンスを生成できるとき 08:52 allSatisfy メソッドとキーパス 10:09 LosslessStringConvertible 11:17 ExpressibleByStringLiteral 12:09 リテラルからの変換構文 15:43 リテラル変換の最適化 19:39 リテラル変換のためのイニシャライザーの直接呼出は禁止 23:02 任意のインスタンスを文字列型のインスタンスに変換する方法 26:24 文字列補完構文 27:07 CustomStringConvertible による文字列取得 28:40 文字列型への変換イニシャライザー 29:53 全ての場面で対応可能な文字列変換 35:05 文字列を生成するか、受け入れるか 37:06 変換イニシャライザーの種類 38:32 次回の展望 ——————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #104
では始めていきましょう。今日は引き続き表示、係数と変数の出力まわりで、これが3回目ぐらいになりましたね。それを最後におさらいするというか、具体的にどういうふうに使うかを見ていこうと思っています。ただ、こういった内容は一度に終わらないことが多くて、今日この話をするよって言ってから何回も回ってやっと話すということがよくあります。なので、今日で終わるかは分かりませんが、あまり気にせず進めていこうと思います。
具体的にはどんな話をしていくかと言うと、これまでの内容として、出力は全てのインスタンスでできるという話や、テキスト出力、その面白いところについてお話ししました。また、出力方式について言語に組み込みで用意されている文字列変換のほかにCustomStringConvertible
というプロトコルを使うことで、独自のテキスト表現をすることができるというお話をしました。ただ、その値そのものを表せるときにはCustomStringConvertible
を使っていいけれど、単純にインスタンスの状況を見たいときなどにはCustomDebugStringConvertible
を使うのが妥当だという説明をしました。では、今日はその続きで、もう少し詳しく変換の仕組みを他の方法と合わせて紹介していこうと思います。
まずは、CustomStringConvertible
の話の続きです。インスタンスそのものの値を表現する場合に関して、もう一つ踏み込んだプロトコルがありますので、それについてお話ししようと思います。これが何か思い浮かぶ方は復習として意識に残りやすくなると思いますし、もし考えていることと話す内容が異なったら、ぜひ思い描いたことも教えてほしいです。
例えば、次のようにValue
という構造体があり、内部にInt
型のプロパティを持っているとします。Value
のインスタンスを作成して、その値を100とします。このValue
型をCustomStringConvertible
に準拠させます。
struct Value: CustomStringConvertible {
var intValue: Int
var description: String {
return "Value is \\(intValue)"
}
}
let value = Value(intValue: 100)
print(value)
とすれば、print
文で出力したときにValue is 100
という文字列が出力されるはずです。ただし、このプロトコルを使う場合、そのインスタンスそのものが特定の値や状態を表していることが前提になります。
もう一つのプロトコルCustomDebugStringConvertible
は、デバッグ用にインスタンスの内容を詳細に出力したいときに使用します。
たとえば次のようにします。
struct Value: CustomDebugStringConvertible {
var intValue: Int
var debugDescription: String {
return "Debug - Value has intValue of \\(intValue)"
}
}
let value = Value(intValue: 100)
print(value)
この場合は、print
文で出力する際にインスタンスの状態をさらに詳しく表示することができます。
では、これらの違いや使い分けについてもっと詳しく見ていきましょう。CustomStringConvertible
は主にユーザー向けの表示をカスタマイズするためのプロトコルで、CustomDebugStringConvertible
はデバッグ時の詳細な情報を表示するためのプロトコルです。
ここまで話してきた内容をもう少し具体的な事例とコード例で確認してみましょう。
もしこれでバリューの値が100と表示される場合、次のようになります。
struct Integer: CustomStringConvertible {
var rawValue: Int
var description: String {
return "Integer value is \\(rawValue)"
}
}
let integer = Integer(rawValue: 100)
print(integer)
このコードでinteger
の値がInteger value is 100
と正しく表示されれば、手順通りに設定できていることになります。以上でCustomStringConvertible
とCustomDebugStringConvertible
の違いと使い方についてお話しました。質問があれば随時お知らせください。 ではエクステンションにしようかな。CustomStringConvertible
でdescription
として、それでrawValue
返してあげれば……のdescription
か、デバッグじゃなくてdescription
。こうやって返してあげればOKですね。ここでCustomDebugStringConvertible
ではなくCustomStringConvertible
を使っているのは、インスタンスそのものとこのテキスト表現が合致するからです。ちゃんと正確に値を表現できているから、これでOKです。デバッグストリングコンバーチブルじゃなくてOKです。
さらにここでね、このインスタンスは100
っていうテキスト表現から値を戻しても成り立つじゃないですか。100
っていうテキスト表現を出力して、そのテキスト表現を回収してもOKといったような時には、もう1個プロトコルがあって、LosslessStringConvertible
です。これがいい感じに機能するんです。
定義を見るとね、CustomStringConvertible
、要は自分自身のインスタンスはその値を的確にテキスト表現可能です。さらにそのテキスト表現、description
を使って再度イニシャライズすることが可能です。一応失敗可能イニシャライザーにはなっているんですけど、そうやってストリングからインスタンスに再変換可能ですというインターフェイス、それを備えてあげることによって統合変換が可能になる。こういったプロトコルが用意されていて、だからイニシャライザーとしてdescription
をこうやってね、イニシャライズするようにしてあげて、これでちょうどいいですね。
rawValue
にそのままdescription
を入れちゃえばいいですね。ただ、数字であるかどうかは判断したいところか。そうするとどうすんだ、rawValue
としてSatisfyAll
、allSatisfy
で8 is number
、これですね、これがガードになるのかな。ガードelse return nil
、これでいいのかな。ここがallSatisfy
でこういうときにはね、丸カッコを全体で作ってもいいけど、キーパスですね。これを使ってね、これでいいですね。これでうまくできたかな。
エラーが残っているような気がするが……rawValue
ではない。間違えた、description
ね。これですね、これでOKだ。こうしてあげるとテキスト表現にもできるし、value = 500
。テキスト表現ここね、テキスト表現で出力もできるし、イニシャライズとして文字列を与えて1000
にしようかな。こうしてあげて1000
という値を文字列から復元することができる。
だからちょっとわかりにくかったね。value
のdescription
で再びvalue = 100
として生成できる。こっちの方がいいですね。こういったのが約束される、概念的に約束されるのがLosslessStringConvertible
。なので逆変換も可能だよっていう場面だったらね、こちらのプロトコルを適用してあげることによって表現できるから、こっちの方が一層応用力の高まるプロトコルになってくるので、おすすめかな。
これぐらいかな、文字列からね、変換する方法として。もう一つね、ExpressibleByStringLiteral
。これも一応軽く紹介しておこうかな。エクステンションInt
としてExpressibleByStringLiteral
ね。これでイニシャライザーとしてStringLiteral
をストリング型で受け取っておこうかな。それでセルフのイニシャライザーのrawValue
、description
でいいな。これでrawValue
にしてこれでね、value
を渡してあげるみたいな風にするとね。そうするとvalue = 3
、Int
としてね、Int
としてストリングリテラルが渡せるんだっけ。どっちのイニシャライザーが走ってるんだこれ。あれ渡せないか。
ストリングリテラルコンバーティブルの場合、どうなった?分かんなくなった。文字列ディスクリプションを取る、リテラルを取る。これは自分で用意してあげないといけないんだっけ?単純なところ分かんなくなったな。
とりあえず変換はできるけどLosslessStringConvertible
を無くしたとすると。ここは32行目はエラーでしたっけ?こっちがダメなのは当たり前として、やっぱ変換できますよね。そうなんだ、なるほど。ちょっと整理しておこう、頭の中。
これは独自の変換イニシャライザーを使った変換ですよね。勘違いしてないよね。このイニシャライザーは何を読んでる?変な定義言っちゃうな。それっぽいイニシャライザーないね。.init
をやろうとするとエラーでしょ。そんなイニシャライザーはないって言われますよね。っていうことは、つまりこのイニシャライザー……じゃない、この今の32行目は、イニシャライザーを読んでいるのではなくて、型変換が走ってる。
つまりlet value
を……数字はどうでもいいや。要はこれと同等なコードが自動的に解釈されて走ってるっていうことになるという、そういう構文的な変換イニシャライザー、変換構文、型変換なのでしょうね。間違ってないよね、きっとここまでね。 なので、こうすると両方とも変換イニシャライザーが走るのかな。こうすると2個走るのかな。こっちローバリューだから大丈夫よね。なるほど。それで、こうやって引数ラベルを受け取らないイニシャライザーを渡してあげると、あれ2個出たね。2個出た。これは自分で読んだんだっけ、ディスクリプション。読んでないね。2個出た。リテラルだからか。リテラルだからか。32行目を復活させても2個。顔文字2個。そういうことか、なるほどね。
リテラル変換は最適化が図られるように変わったんですよね。例えば Int64
や UInt
とか。ちょっと変なコードになってますけど UInt.max
。これリテラルじゃないとダメだ。これでちょっと雑にいきますけど、整数を超える範囲の数字とか、だから Int.max
を取ってあげて、これをコピーすればいいんだね。こうしてあげて、あとここはストリングリテラルじゃなくてインテージャーリテラルにこうして。たとえば今 Int.max
ですけど、これを Int.max
を1個超えた感じにしたときに、ちゃんと動きますけど、昔は動かなかったんですよね。
まず先にここを Int
型で解釈しようとしてしまって、この段階でオーバーフローを起こすっていうね。だからこのコードをちゃんと動くようにしたいときには as UInt
みたいな書き方をしないといけなかった頃があったけれど、でもここのリテラルは UInt
として扱うべきものだから、UInt
に解釈するよみたいなコンパイラーがちゃんと気を利かせてくれるようになったおかげで、ちゃんとこのコードでもリテラル変換、ExpressibleByIntegerLiteral
の型変換がちゃんと走ってしっかりとイニシャライズする。要は init(integerLiteral:)
こっちが呼ばれるようにちゃんと解釈してくれるようになったという経緯があるので、きっと33行目も StringLiteral
のほうを読んであげるよっていうふうになってるんでしょうね。
なるほどね。分かりやすいような、考え出すとこんがらがるよ。でも最適ですね。話を戻そう。どんな話だっけ。StringLiteral
で LosslessStringConvertible
の紹介をして、ついでに ExpressibleByStringLiteral
も紹介しておこうというお話でしたね。それで、StringLiteralConvertible
を書いてあげると同じような書き方ができるけど、これはリテラルしか取れないよっていうお話をしようとしたんですけど、それでいいのか。
32行目。この32行目は LosslessStringConvertible
のこの8行目のイニシャライザーのおかげで実現できてて、同じようにテキストからインスタンス化できるっていう方法として ExpressibleByStringLiteral
もあるけれど、分かる人にとっては別に全然確認するまでもないかもしれないですが、ExpressibleByStringLiteral
の場合はこうやってストリングのインスタンスを取ろうとしてしまうと使えないので、ちょっと目的の違うイニシャライザーだよということですね。
ついでに余談だけれど、StringLiteral
っていうラベルを付けてしまえばコンパイル通りますよね。ちょっとね。通るよね。通んないか。エラーでちゃんと起こられるのかな。整数がディスクリプションを持っていない。そうか。これ今コメントアウトしちゃったからだ。まあいいか。StringInterpolation
にしよう。こうね。これでコンパイル通っちゃうよね。コンパイル通っちゃってちゃんと動いちゃうんですけど、StringLiteral
のイニシャライザーを呼ぶっていうのは直接やっちゃいけないよって、多分コメントに書いてあるかな。書いてなくてもやんない方がいいと思うんですけど、このイニシャライザー、書いてないか。イギリスを。あれ書いてないね。でも呼んじゃいけないですよね。どっかにいろいろ書いてある。自分でイニシャライザー呼んじゃいけないってどっかで書いてあった気がしますよね。どこだったかな。でもなんか書いてないね。いいや。
基本的にどっかに書いてあった気がするんでやんない方がいいし、そもそもリテラルから変換するための裏で呼ばれるイニシャライザーなので、直接扱うものとしては考えられていないと思われる。あくまでも自分の想像みたいな感じに話になっちゃいますけど、公式的な根拠がないので。ここに出てたりするかな。何か一生懸命定義たどってたけど、クイックヘルプに出てますね。
とりあえずね、直接呼ぶべきではないので。やっぱり、イニシャライザーのほうに書いてありました。本当。それと同じノリでしょうね。一応見ておけますかね。イニシャライザーのほうに書いてあるのかな。それとも。あったこれだ。これですね。直接呼ばないでくれって代わりにリテラルを使って変換してくれみたいな。こんな感じ。Modulate
もきっとそうなのでしょう。OKね。なので、こうやって呼んであげる。もしくはこうやって呼んであげるっていう感じ。32行目はNGだ。こういうことですね。 さて、面白い内容が出てきました。Modulate
に変換するときの話です。Lossless String Convertible
でも良いのですが、Custom String Convertible
でも同じです。前回お話しした他の Convertible
でも同様です。Modulate
に変換する部分は、意外と簡単に見えるかもしれませんが、具体的に Modulate
に変換する方法をおさらいしたいと思います。
Lossless String Convertible
は Custom String Convertible
と比べて複雑になるので、今回は Custom String Convertible
についてお話しします。これを Modulate
化するという話を見ていきましょう。
例えば、バリューとしてインテジャーとして 555
という値があったときに、これを Modulate
に変える方法です。シンプルな方法として、今までこの勉強会の中で僕らが使ってきた方法は、プリントして Modulate
変換していたわけです。例えば、print(555)
という形で値を表示していました。
この方法を実際に Modulate
として扱いたいときには、色々と方法があります。それをおさらいしつつ、どの方法が一番良いのか見ていきましょう。
まず print
は OK です。プリントすると Any 型を受け取って適切に表示してくれるので問題ありません。しかし、文字列型にバリューを文字列として入れるのは少し難しいです。例えば、String
型にバリュー型を文字列表現として入れる場合、どの方法が思い浮かびますか?
いくつか方法があります。まず一つ目は、String Interpolation
です。例えば、let str = "\\(555)"
という形にします。String Interpolation
は Any 型を受け取り、全てのインスタンスに対して適切な文字列表現に変えることができます。
他の方法としては、バリューの description
を使う方法があります。これを思い浮かぶ人もいるでしょう。しかし、この方法は Custom String Convertible
を意識していないと難しいかもしれません。
自分としては18番のほう(descriptionを使うほう)が好きです。17番(String Interpolationを使うほう)はちょっと感覚的にダサいと思うことがあります。18番のほうが軽量ですし、descriptionなので分かりやすいです。ただ、descriptionを使うと期待通りに動かない場合もあるかもしれません。
もう一つ方法があります。String
の初期化方法を使う方法です。例えば、String(555)
のように。これも有効な方法です。
この3つの方法(String Interpolation
、description
、String
の初期化方法)が、Custom String Convertible
の方法の中では妥当な選択です。それぞれ長所がありますが、descriptionを使う方法はとても最適な選択です。
ただし、この方法が使えない場合もあります。例えば、独自のナンバー型などがあり、その型が Custom String Convertible
に準拠していない場合です。この場合、String Interpolation
や description
を使った方法では上手くいかないかもしれません。
このように、いくつかの方法をおさらいしました。具体的な実装や状況に応じて使い分けることが重要です。 ただ、description
を持っていないんで22行目では使えないんですよね。だから何っていう話にもなりはするんですけど、一般的な概念で見たときに必ずしもCustomStringConvertible
に準拠していないことがあります。そうすると22行目の道が立たれるので、22行目は特別な場面用になってるわけです。
要はね、文字列にインスタンスをどんなものでも変換できるっていう価値観の中で、じゃあインスタンスを文字列として取得しましょう、という発想になったときには21行目か23行目、この2つが最適な選択肢になってくると思うんですけどどうですかね。まあこういう背景があるので、description
を使うっていうのは悪いことじゃないし、使えるところは使っていった方が最適なんですけれど、突破していくっていうところだけは独自の文字列変換を備えたインスタンスにしか使えなくなっちゃうよということはね、ふとこのdescription
を使うたびに思い浮かべると今後いいことがあるんじゃないかなと思います。些細ないいことだと思うんですけどね。
こんな感じなのでどこまで直そうかな。でも、とりあえず、この3つの変換方法がある。で、さっきお話聞かせてもらったアイデア、String
のイニシャライザーの話、あれもいいですね。String
変換の組み込みの機構的なところからは外れますけど、でもアイデアとしてはとても適切で、文字列変換をあらかじめ用意してあげるっていうね。
例えば、整数型を作ったときに、ついでにエクステンションでString
を拡張しておいて、イニシャライザーとして変換イニシャライザーを整数型に対してあらかじめ用意しておいて、これでself
にrawValue
を入れてあげればいいのかな。まあこういうふうに変換イニシャライザーを用意しておくことによって、正確なString
変換を用意しておく。こうやってString
割り、これも24行目のお話と似たようなイメージで特化したものにはなるんですけど、文字列に変換するっていうのが最も綺麗に表現できている変換イニシャライザーなのでね。なのでこれが理想郷っていう感じがするんですけど、どうですかね。
この24行目と26行目は結構、このString
に限らず、自身がString
を表現できるのか、String
が自身を受け入れてくれるかの逆方向じゃないか。24行目はString
を生成可能、26行目はString
がそのrawValue
を整数型を受け入れられる。これどっちがいいんだろうって結構悩むんですよね。どっちかというと24行目の方が好きで、自身が表現できた方がコード量が若干短い。数行ですけど短いかなっていうので24行目の方が好きですけど、悩みますね。
そうね、利点としてさっきのplus
オブジェクトみたいなのがあって、これも仮にね、String
変換できるっていうような時を考えると、例えばね、こうやって、なんかまどろっこしくしたな。ま、何かを入れるっていうような形にこだわってるわけではないんで気にしないでくださいね。こういうようなのがあった時には、その同じように統一感が出てくるので、まず統一感という価値観では、さっきdescription
使えなかったオブジェクトね、統一感という観点ではとてもいい感じになるので、統一感がないとvalue
の時にはdescription
使えるな、オブジェクトの時には使えないなとか、そういった思考が一歩挟まれちゃうんでね。そういった面では34行目がまずいい気がするのと、あとアプローチが逆という観点ですけれども、SwiftではAPIデザインガイドラインで、文字列に値を壊さずに変換できるタイププリザービングタイプコンバージョンっていう時には、String
イニシャライザーを増やして、かつラベルを省略するっていう基本があるので、そのデザインガイドラインに習うと、String
にvalue
が変換できるからdescription
っていう発想と、value
がString
に変換できるから変換イニシャライザーっていう発想でね、アプローチが逆にならないとも捉えられる。そういった感じで捉えていくと、必ずしも逆にならないというか、もしかするとね、いい感じでvalue
を文字列に変換できるんだから34行っていう捉え方もできるんで、一手間押しまずに済む状況であれば、この34行目の変換イニシャライザー、これを作ってあげると、最も的確に意図が届いていくんじゃないかなと。
とりあえずね、今のところ自分はそんな風に感じるかな。今日は具体的にインスタンスを文字列として取得するっていう話を見ていきましたけれども、これもやっぱりもうちょっと話したいところがあるね。もう1回次回続けて話していこうかなと思います。
じゃあ1時間になりましたので、今日の勉強会これで終わりにしますね。お疲れ様でした。ありがとうございました。