https://youtu.be/8ldxSQ4DZ80
今回は A Swift Tour
の 列挙型
について引き続き眺めていきますけれど、それの Raw 型
について詳しく見ていく回になりそうです。何気なく使えてそれで支障のない機能ですけれど、せっかくなのでより細やかなところをおさえておきましょう。どうぞよろしくお願いしますね。
前々回に話したことがらも改めて登場するところも多そうなので、それに参加してくれた方は復習気分で見ていけるかもしれないです。
———————————————————————————— 熊谷さんのやさしい Swift 勉強会 #56
00:00 開始 00:56 RawRepresentable の使い道 08:04 列挙型の Raw 型の特徴 09:24 Raw 値の明記 13:44 Raw 値として使える型 14:21 Raw 値で浮動小数点数を使う 15:50 数学定数の定義方法 16:53 Raw 型に任意の型を指定する 20:46 Raw 値がインスタンス化されるタイミング 22:55 活用場面の試行錯誤 26:55 複数のリテラルに対応した Raw 型 29:51 Raw 値に指定できるのはリテラルのみ 31:54 いろいろな表現で Raw 値を表す 35:19 Raw 値から列挙子を生成 38:21 整数リテラルと浮動小数点数リテラルの同値判定 45:33 混在するリテラルから列挙子が作られる流れ 48:35 複数のリテラルに対応することの妥当性 50:47 クロージング ————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #56
では、始めていきましょう。今日はセクションとしては列挙型と構造体についてですが、そのうちの構造体についてのお話です。構造体の話は前回、前々回くらいから入りましたが、今日もその中のローバリュー型の話がスライドで3つほど続きます。今までにも少し話してきましたが、改めてスライドに沿って見ていこうと思います。
列挙型のローバリュー、ローチ、ローチよりもローバリューと言った方が通じやすいですかね。このあたりのお話ですが、実際にローバリューはどのくらい使われているのでしょうか。最近は、コーダブル(Codable)によってローバリューを使う機会が増えましたが、それ以外の場面でローバリューがどのくらい使われるのでしょうか。列挙型に限らず、ロアリープレゼンタブル(RawRepresentable)を活用した事例ってあまり見かけない気がします。個人的にはそのような実例をあまり思い出せません。
もちろん、これはあくまでも個人的なプログラミングのスタイルかもしれないです。プロトコル RawRepresentable
をつけておくか、みたいなそんな感じでコードを書いたことはあっても、日常的にローバリューを使っているわけではないです。周りの状況も似たようなものかもしれません。
実際に自分で RawRepresentable
プロトコルに準拠させて作った型を使うことはあまりありませんが、ローバリューを呼び出したり設定したりすることはよくあります。例えば、フロントエンドの関係だったり、ID的な役割や名前をつけるために使ったりします。
Codable
以外の活用では、文字列で名前をつける時に使うことがありますね。例えば、IDのタイプを明記的にするために RawRepresentable
を使い、そのIDをストリング型として定義し、エンコードやデコードの際にストリングに変換することもあります。具体的には、これにより異なるIDを混入させることを防ぐ目的で使ったりします。これを普通の構造体(struct)に対しても行うことができます。
では、スライドに戻ります。今回は列挙型に絡んだローバリューの話です。定義の仕方については以前にもお話ししましたが、コロンを打って型を書いてあげると、それがローバリュー型として採用され、列挙型は自動的に RawRepresentable
に準拠します。ケースごとに定義されたローバリュー型の値が設定されます。設定しなければケースは0から順に値が割り当てられますが、イコールで値をカスタマイズすることも可能です。
具体的なコードの例を紹介すると、例えば以下のような形になります。
enum Rank: Int {
case first = 1
case second
case third
}
このように書くと、first
は1、second
は2、third
は3のように順に値が割り当てられます。しかし、カスタム値を指定することもできます。例えば、以下のように。
enum Rank: Int {
case first = 1
case second = 100
case third
}
この場合、first
は1、second
は100、third
は101になります。このようにローバリューの値を自由に設定することができます。 それで、4は 101
、その手前の2はこんな感じで1ですね。0, 1, 100, 101
みたいな感じです。では、ここの6のところで200
とか設定してあげると、そのようなこともちゃんとできます。そうすると、ランクの例えば7のローバリューは201
というふうに途中途中で設定を変えていくことができます。例えば、ここで102
とかにすると、すでにこの5で102
が割り当てられているためエラーになります。重複してしまうからローバリューだね、というふうなエラーが起こります。
整数型でやると何となく特別感が出てきますが、文字列型だと普通に3に対して例えば「丸さん」とか「赤子さん」とかにすることができます。ここ6のときには絵文字など何でもいいですけど、特別扱いしたいときだけ設定するということです。よく文字列ではやると思います、特にコーダブルだとね。こういったことを調べていくと、整数型で途中から明記することも特殊な書き方ではないなという感じがしてくるかもしれません。
整数型の場合は、次の列挙子が前の列挙子の次の値という特別ルールがあるので若干見た目が変わってきます。文字列型も同様に、何も指定しないとその列挙子そのものになるというルールがあるので、ここで3に対して例えば「変」と指定すると重複するよと言われます。やったことなかったので分かりませんが、ちゃんとそのようになります。同じような感覚です。
さて、Swift
ではどのような型を列挙型のロー型(基底型)として使えるかというと、C言語
とは異なり、int型
だけでなく、int8型
も使えますし、文字列型も使えます。他にも不動小数点数型も使えます。これが面白いところです。私は不動小数点数をロー型として指定したことはありませんが、例えばケースAとしてダブル型などでやってみることができます。例えばパイなら3.14
とか、自然対数のE
などですね。E
っていくつでしたっけ?2.7なんぼでしたか。すごいですね、よく知っていますね。このように表現することもできますが、最近のSwift
では別の方法で表現することが一般的です。
Swift
では、ダブル型にパイという定数を持たせて拡張するのが一般的です。例えば、extension Double { static let pi = 3.14 }
のようにしてパイを持たせることができます。余談ですが、ロー型というのは基本的に、文字列型、整数型、不動小数点リテラルが指定できるようになっています。
それ以外の型、例えばオプショナル型を指定したらどうなるのでしょう。やったことがないのでわかりませんが、ダメみたいですね。この場合、エラーになります。適切な型でないため、String
やInteger
、またはフローティングポイントリテラルで表現できる型でなければなりません。したがって、例えば自作の型を作ってインテジャーリテラルに対応させることができます。
例えば、struct MyData: ExpressibleByIntegerLiteral { typealias IntegerLiteralType = Int; init(integerLiteral value: Int) { /* 初期化処理 */ } }
のように定義して、使うことができますが、Equatable
プロトコルが必要ですと言われます。 ですので、Equatableも載せてもうちょっと試してみます。set v = MyData
、あ、間違えました。const v
にしましょう。できたできた、こんなふうに独自のデータ型をRaw型として指定できるんです。インテジャーリテラルやストリングリテラル、フロートリテラルに対応していれば、ちゃんと動作します。これでRawValueを取ることができます。
例えば、MyData
のxがInt8
型で持たせてみましょう。そして、xをvalue
で初期化すると、このv
のRawValueは3を持ったMyData
になります。ここでxを出さないといけないので、そうすると3になります。このMyData
はいつインスタンス化されるかというと、前回、もしくは前々回にお話したように、RawValueにアクセスしたときに初めてインスタンス化されます。それまではRaw型に定義されている値とか無関係に、この列挙子で管理されます。
列挙型のサイズについても話したと思いますが、例えばInt8
だとすると、ケースが1個の場合、列挙型のサイズが0になります。これをconst
にするとサイズが0です。こういう感じで、RawValueを取ったときに初めてサイズが変わります。ですので、独自のRaw型を作って使うとき、その型が重たいものであったとしても、RawValueとして取得する時点までメモリや処理が走らないのです。イニシャライズ処理が走らないのでうまく使えば効率的に動くというものです。
例えば、MyData
の代わりにストーリーボードアイテムやURLからロードするようなデータ型を作って、それをString
リテラルで初期化するようにすることができます。しかし、既存のデータを拡張するのは少しやりすぎな感じがするので、やめておきます。それでも、例えばサウンドデータを大きくロードして使用するようにすることも可能です。このように、重たい処理を初期化の段階で行わず、必要な際にのみ行うことが可能です。
リテラルに対応する型として、例えばInt
リテラルとExpressibleByStringLiteral
に対応するようにすることもできます。型は複数のリテラルに対応可能で、このようにリテラル変換とEquatableに対応していれば利用できます。
このような工夫を通じて、柔軟で効率的なコードを書くことができます。何か質問があれば教えてください。 これをやったときにどうなるかというところも紹介しておきたいんですが、まず動かしてみますね。こうやって動きますけど、ここで何かケースを作ったとして、Int
リテラルも対応しているので、Int
リテラルも指定できるという作り方もできます。
もちろん、Double
型にも対応させれば、Double
型もちゃんと動きます。フローティングリテラルのあたり、今のところ使い道をちゃんと考えたいいアイデアはないんですけど、場合によっては何かいいことがあるのかもしれないですね。とりあえず、こういうこともできるということです。
例えばリテラルを混ぜていけるんですが、このときのローバリュー(rawValue
)がどうなっているのかというのは、混乱しがちです。実際には何のこともなく、サウンドビープもサウンドノーティフィケーションも、型は混在することなく設定できます。あ、列挙子ですね。列挙子になるローバリューを出していないからです。
ローバリューはどうなるかというと、どれもすべてSoundData
型になります。こういった感じで、特に複雑なことは起こりません。
もう1つ、独自の型を作り始めると勘違いしがちなんですが、ここでSoundData
型のインスタンスを入れようとすると、これはエラーになります。ローバリューにはリテラルしか指定できないというエラーです。これ、自分が初めてこのエラーに遭遇したときにはすごく驚きました。今までローバリューを指定したときに、リテラルしか書いていなかったんですよ。
Int
型のインスタンスを明記するということをしたことがなかったので、例えば値を変数として計算するようなことは許されていない、ということになります。これは面白いですね。何にしても、必ずリテラルしか指定できないのです。コンパイラー側、つまりSwift側では列挙詞に対応する値はリテラルと対応づけて管理されています。ローバリューにアクセスしたときに初めてリテラルからインスタンスが作られるという動きです。
ここまでわかってくると、混乱しなくなります。非常に自然な流れが作られているように感じます。
コメントをいただいている中で、型の話の際に「明示的に」と言っていましたよね。いくつものローバリュー、つまり複数の型に対応できる羅型を自前で作った場合、こういった状況が起こり得ると思います。Double
型もいけますね。Double
型はInteger
リテラルとフローティングポイントリテラルに対応しているので、可能です。
こうやっていろいろな表現で列挙子を書くことができるようになっていて、適切なイニシャライザーがちゃんと呼ばれるので、このあたりを意識してコードを書いてみると面白い表現ができます。場合によっては効果的なコードが書けるかもしれません。今まで書けたことはないですが。 なので、ちょっと面白いかもしれません。これに興味がある方はぜひやってみてください。
あと、ローチのお話がありましたね。次にその話に移りたいと思います。ロー型を持たせてローチに対応させると、このローチから列挙型を作ることもできるという話です。これはローリプレゼンタブルのお話に関わってきますね。
ロー型を指定するとローリプレゼンタブルに準拠することになり、ローリプレゼンタブルの仕様に従うことになります。具体的には、ローバリューの型とローバリューの型からインスタンス化するイニシャライザー、そしてローバリューのプロパティ、この3つで構成されます。
ロー型を持たせた列挙型はイニシャライザーを持つようになり、ローバリューからイニシャライズが可能となります。ただし、例えば 3.6
のように複数のリテラルがある場合、プレイグラウンドが動かなくなることがあります。急に混乱しているかもしれませんが、複数のリテラルがある場合にどうなるかをちゃんと考える必要があります。
実際にローバリュー AAA
などの2つのリテラルを動かした場合、自然に動くかなと思うのですが、サウンドのケースに 3
という整数が入ると思います。このとき、 3.0
のようなものが作れるのか疑問に思う方もいるかもしれません。興味深いことに、普通はできそうに見えるのですが、実際にはできません。エラーが出て "ユニークではない" というメッセージが表示されます。
これはユーザーにとって混乱を招く原因になります。やり方がいくつか考えられますが、どのイニシャライザーを通るかは不明瞭です。お話していたデフォルトの型推論が影響していると思われます。例えば、フロートリテラルタイプやイントリテラルタイプを指定することで影響が出るかもしれません。
インテジャーリテラルタイプにダブルを指定しても影響が出るかどうかは試してみる必要があります。タイプオブ 3.5
を確認すると、このコードがあると ダブル型
になります。この型の推論は、元々の型をキャストしないと辻褄が合わないケースが多いです。
実際のコードでは、「3.0と3」が同じ値と見なされないように工夫する必要があります。サウンドデータ型の設計としては、リテラルによって内部の列挙値が変わる場合、ユニークになります。例えば、列挙型のケースとしてイント型やダブル型を持たせ、リテラルによって型を切り替える設計がユニークな状態を保つ方法の一例です。 それなのにエラーが出るのは、ストリングリテラルを間違えたからですね。ここは、アザね。フロートの時がこうやって開けるじゃないですか。そうすると、サウンドデータ型からすると 3
と 3.0
はユニークなのに、その前段階でエラーが出ちゃうというのは、ちょっとやりすぎ感があるかなとは思ったんですけど。
確かにここで話している内容かもしれないですね。ここでサウンドとか、この部分で何型に解釈されるかというお話。そうですね、そこがこのあたりのことですね。この時ってローバリュー型はサウンドデータ型だから、ここでサウンドデータ型として解釈されることになりそうだな、そうすると大丈夫じゃない?これならほら、インテージャーとしての判断になるじゃないですか。ここは大丈夫そうな気がするな。
そっか、そういう解釈なんだ。まだ自分の中で混乱が拭いきれてない。そうすると、ローバリューが...あれ、これどうなるんだ?サウンドがローレプレゼンタブルの時にローバリューから列挙詞が決められるんでしょ。この時、サウンドデータ型に解釈したとすると、どう判断するんだと思ったけど、サウンドデータ型としてこの 3
なり 3.6
なり AAA
なりを持ったサウンドデータ型がインスタンス化されて、ローバリューイニシャライザーに渡ってきて、その上でケース分と比較するんだ。
だから、init?()
ローバリュー サウンドデータ、ここでスイッチ。サウンドデータだからローバリュー、ローバリューでケースで 3
だったらという風になっていくわけですけど、こんなノリで全部書いていくんです。この時のケース 3
っていう動きはどういう動きをするかというと、リテラルからサウンドデータが作られて、リテラルからだからインテジャーリテラル。そしてこう呼ばれてイニシャライズされたものとこれと、サウンドデータだからローバリューが一致するかという比較をして、あっていったらノーティフィケーションという動きをするんだ。やっと整理できた。なるほど、なるほど。
そのためにイコータブルが必要なんですね。なるほど、ありがとうございます。じゃあ全然ちゃんと動くわ。そうすると、なおさら 3
と 3.0
を区別する必要は無さそうだし、同一視してしまうことによって本来サウンドデータ型が表現できる 3
と 3.0
という2つの表現力を抑え込んでしまっている。言語仕様が邪魔している感じがしますね。こういう実装をしないといけないというのが、いろいろ工夫して通りそうな感じはするんですけど、こういう工夫をしないといけないということ自体がダメみたいな感じなんですかね、実装的には。
どうなんですかね。でも、バリアント型ならあり得るじゃないですか。例えば、JSValue
JavaScriptコアのJSValue
みたいに文字も整数も。JSValue
は内部でダブル。でも、バリアント型を作ったときにはあり得るのかもしれませんね。バリアント型だったとすると、バリアント型で内部で Any
としてひっくるめて証言している NSNumber
的なものだったらわかるんですけど、これがさっきの別居型みたいに、バリアント型で内部でちゃんと型情報を別個に持っておくみたいな表現のときには自然な感じがするので、言語仕様が若干おかしい気がします。
だから技量があれば、ここおかしいからちゃんとユニークなものとして扱ったらいいんじゃないみたいなプロポーザルを出せば通るかもしれない気もする。どうなんだろう。無理やり感というより言語仕様が邪魔しているなという印象が、今のところ個人的にはするかなという感じです。
ありがとうございます。こちらこそありがとうございます。こんな感じで時間にもなりましたし、いい感じで話せた気もするので、今日はこれで終わりにしましょうかね。お疲れ様でした。ありがとうございました。