https://youtu.be/QEi0J45XYd8
今回も A Swift Tour
の 列挙型
から 列挙子
についてみていきます。これまでに話した事柄もあちらこちらに出てきますので、それらは軽くおさらいしたりしながら、列挙子の全体的な特徴を眺めていこうと思います。どうぞよろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #57
00:00 開始 00:26 オリエンテーション 01:36 Raw 値からのインスタンス生成 02:46 列挙子を Raw 値からインスタンス化するときの処理の流れ 04:09 RawRepresentable 09:19 Raw 値から列挙子を作れない場合 10:34 失敗可能イニシャライザーにおける return nil 11:20 共変 (Covariant) 16:23 init? の共変性 18:23 質疑応答 19:52 Raw 値で表現可能なときとそうでないとき 24:32 オプショナル型の共変性 25:56 列挙子自体が値 27:14 意味のある Raw 値が存在しない時 28:19 Swift 以外での列挙型 32:02 Raw 値を割り当てることについての考察 33:28 String の Raw 型で表現する例 34:59 CustomStringConvertible 38:07 声のバランス要調整 41:13 Codable と列挙子の関係 42:13 Raw 値に頼らない表現例 44:02 複数の文字列表現がある場面 45:37 どの実装が適切か考えていくことが大切になりそう 46:16 質疑応答 49:30 列挙子の取り柄を生かすために Raw 値を避ける 50:58 クロージング ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #57
【列挙型の話】
今日は列挙型の続きですね。そんなに難しい話が出てくるような感じではないので、おさらい的な感じで進めていこうと思います。今日、列挙型の回に初めて来られた方もいらっしゃるので、復習がてら話していこうかなと思います。
この勉強会はずっとやっていますが、以前に話したこととかも全然話題に挙げてもらって大丈夫です。また、今日だけの範囲においても、ちょっと前に出てきた話題とか、言い逃した話題とか、そういった内容も遠慮なく話してもらってオッケーです。よくあるじゃないですか、勉強会で「あの話題前に出てきてたのかな」というような感じで。今日の中で気にしていると、もったいないので、前に出てきたかもなーとか、ちょっと席外してたんだけど、という感じでも遠慮なく話してもらえれば、そこから話が広がるかなと思います。
では、スライドの方に入っていきますけれども、前回にも話したお話ですけど、列挙型で「ローバリュー」を設定してローチを指定できるという話をしましたね。それをローバリューから列挙子を生成することもできるという話。このあたり、前回は複数のリテラルを混合した自作の型を使っての話だったので、ややこしかった部分もありました。もっとシンプルな例で、インスタンス化する方法を軽くおさらいしておこうかなと思います。
具体的な動きの話ですね。まず列挙型があって、それがローバリューを持っていて、ケースとしていくつかの値を持っている状態。こういう時に、この列挙子をローバリューからインスタンス化できるというお話です。ローバリューで例えば 1
とかすると B
が返る、こういうお話。これが具体的にどう動いているかを軽くお話ししておくと、まずこの Enumerations に実装されたローバリューを引数にとるイニシャライザーがどのように定義されているかです。
これは init
として、変換できない可能性もある、失敗可能イニシャライザーになっていて、ローバリューをローバリュー型として受け取って、変換処理を行うという動きになっています。ローバリュー型というのは、暗黙的に列挙型が RawRepresentable
に準拠していて、この定義の中で規定されているアソシエイティブタイプ、これがローバリュー型です。それを受け取るイニシャライザーが定義されます。
これが列挙型だと暗黙的に列挙型として設定され、タイプエリアとしてローバリューが Int
になり、計算型プロパティ rawValue
がローバリュー型として扱われます。内部的には A
だったら例えば 0
とか、あらかじめ設定しておいたローバリューが返されるという風になっています。イニシャライザーの方がどうなっているかというと、その前に呼び出すところからいきますかね。
ローバリュー型が引数として取られるので、これがローバリュー型になります。Enumerations のローバリュー型として解釈されます。今回は Int
型なので、Int
型として解釈され、後は Int
型のリテラルが変換されるという動きです。そしてイニシャライザーに渡ってきて、このローバリューがリテラルに割り当てられる前回お話ししたように、列挙子にはインスタンスが割り当てられているのではなく、リテラルが関連付けられている状態です。これがまだインスタンス化されていないので、ローバリューからイニシャライザーするときには、スイッチでローバリューを受け取り、ローバリュー型 Int
が関連付けられたリテラルを初期化し、それと合致していたら該当する列挙子を自分に割り当てる動きになります。
すべて丁寧に書いてあげると、Int
型を生成してそれとローバリューを比較し、合致したらそれで初期化するという動きになります。こういう風にしてローバリューから列挙子をイニシャライズしていくという働きを実装しているわけですね。ローバリューからイニシャライズするイニシャライザーを呼んだときには、まずリテラルがイニシャライザーに変換され、かつイニシャライズの過程の中で比較したいリテラル値がインスタンス化され、Equatable
で比較される必要があるというのがポイントです。
重たいローバリュー型を使う場面は今まであまりなかったですけど、そういった場面のときにはこういう動きを把握しておくと、コードの書き方が若干変わってくるかもしれません。こんな感じで、ローバリューの変換についてはオッケーですね。もう一つの話題についてもお話しさせていただきたいと思います。 失敗可能イニシャライザーのリターンnil
というのは、初期化ができなかったことをコンパイラに伝えるための便宜上の表現です。実際にはnil
を返しているわけではなく、初期化が失敗することを意味しています。これが非常に重要なポイントです。
次に、オプショナル型についてお話しします。オプショナル型は、Swiftでよく目にするものですが、RawRepresentable
との関連について考えてみます。RawRepresentable
の定義では、失敗可能イニシャライザーが求められます。しかし、場合によってはそのイニシャライザーが失敗しないこともあります。例えば、TwitterのIDを表現するための構造体を作る場合を考えてみましょう。この構造体はRawRepresentable
に準拠し、ローバリューをString
型にするとしましょう。
struct TwitterID: RawRepresentable {
var rawValue: String
init?(rawValue: String) {
// IDの初期化論理
self.rawValue = rawValue
}
}
この場合、rawValue
がString
型なので、値が文字列である限り初期化は失敗しないわけです。つまり、失敗可能イニシャライザーである必要はありません。しかし、RawRepresentable
が要求するのは失敗可能イニシャライザーです。そのため、イニシャライザーが失敗しないにもかかわらず、プロトコルに準拠するために失敗可能イニシャライザーを定義することが求められます。
このとき、失敗可能イニシャライザーを定義したとしてもエラーにはならず、他のプロトコルでも同じように応用できます。例えば、IDが取得できなかった場合の処理が不要になるため、コードがシンプルになります。
また、init?
を要求しているプロトコルに対して、失敗しないイニシャライザーを提供してもプロトコルに準拠することができます。これにより、オプショナルバインディングやオプショナルチェーニングの手間を省くことができるのです。
例えば次のような具合です。
struct User: MyProtocol {
var id: String
init(id: String) {
self.id = id
}
init?(rawValue: String) {
self.id = rawValue
}
}
この場合でも、init
が失敗しない場合、必要に応じてinit?
としてプロトコルに準拠させることが可能です。そして、P型として扱う場合にはオプショナルでない型が得られるため、処理が確実に行われるようになります。
メタタイプの扱いでも同様です。
let user: User? = User(id: "123")
このように、汎用的なケースにおいては失敗する可能性があるものの、具体的なケースでは失敗しないことが保証されているため、安心して使えます。このように、RawRepresentable
やそれを要求するプロトコルに応じて、どのように対応すればよいかを理解しておくことが重要です。
コメントも確認すると、init
からinit?
にすることはできませんが、その逆は可能であることが再確認できます。これがSwiftの柔軟な設計の一つと言えます。 あとは純粋に RawRepresentable
の例を挙げるならば、この要求するローバリュー型が、それが準拠した型で表現できるとは限らないということが想定されます。つまり、イニシャライズできないかもしれないという感じになります。
ここだけ着目すると、必然的に失敗する可能性が出てきます。具体的なところを説明すると、例えばドラフト表とIDをローバリューで表現する場合、表とIDを列挙型のように扱うことができます。例えば Yes
または No
、または Other
といった感じですね。
Bool
型の書き方とは少し違いますが、ここでは無理やりな表現にするため、3パターンしか表現できないようにします。そして、ローバリューを Int
型で扱いたいという話になります。
typealias RawValue = Int
とします。Int
型は64ビットの範囲で広大なエリアを表現しますが、この行動体が表現する値は限られています。そうすると、表現できないローバリューが出てくる可能性があるため、失敗するかもしれない設計にしなければなりません。
具体的には、スイッチケースで以下のようにローバリューを扱います:
switch rawValue {
case -1:
return nil
case 0:
self = .no
case 1:
self = .yes
default:
return nil
}
こうすることにより、表現に漏れたときには失敗するしかなくなります。これによって失敗する可能性があるということです。
これを失敗しないようにする設計方法もあります。例えば、0
未満なら nil
、1
以上なら true
とすることで、ケースにデフォルトは使いません。このように設計することによって失敗しようがなくなります。
ちなみに、これは質問とは別の余談ですが、「共変」というキーワードがあります。共変性と反変性です。「共に変化する」という意味で、英語では Covariant
と Contravariant
といいます。もし間違っていたら、後で訂正しますね。
このあたりが失敗可能なイニシャライザーの面白いところです。ファンクションで例えば Int?
を渡す場合、Int
型がオプショナルに渡せることもあります。ここでもさりげなくオプショナル型に対して普通の Int
を渡せるという感じです。
何気なく使っていますが、いざ使おうとすると非常に特別な感じがすることも多いと思います。このようなノリで init?
も使っていくとプロトコルと関連した面白さが見えてくるかもしれません。
ローバリューからインスタンス化に関連した話はこのくらいでよいでしょう。次の話題に移りますね。
前回もお話ししましたが、Swiftの列挙型はその列挙子自体が値です。C言語のような過去の言語を使っていた方は、列挙子は Int
型だという先入観があるかもしれません。
ですが、Swiftの場合、列挙子そのものが値として存在します。この概念に慣れるまで時間がかかるかもしれませんが、一度慣れると特に問題はありません。列挙子に対して意味のある値が存在しない場合、それを提供する必要はありません。
Apple のドキュメントには、「必ずしも提供する必要はない」と記載されています。個人的には、意味のあるローバリューがない場合は提供すべきではないぐらいの認識でよいかと思います。 今回の表現ですが、スライドにある例がちょっと面白いですね。これは賛否両論ある例かもしれませんが、コメントも面白いです。
確かに、ローバリューがなくても列挙型として成立する主流言語は思いつきませんね。C++もCと同様です。最近C++の列挙型を見ていて面白い発見がありました。今のC++にはenum class
というのがあり、これがローバリューがint
型に限らず、int8
型など他にも指定できるようになっています。enum class
はC++11で導入されたもので、モダンにしようとする意図があります。規定の整数型を指定することができるので、規定しないということもできるかもしれません。
ただし、C++でenum class
を使うと、普通の列挙型と異なり、int
型に素直にキャストできず、若干のキャスト技術が必要になります。これは、列挙型をより安全に使おうという意図があるからです。
コメントでJavaの列挙型についても触れられていました。私は昔Javaをよく使っていましたが、最近使わなくなってしまいましたね。Javaの列挙型もCとは異なる感じがあります。定数をまとめて保持する方法として有効です。また、数値として変換できたり、文字列の値を定義することもできます。Javaの列挙型も興味深いですね。
話をSwiftに戻します。フロー型がないという場面に関しては、特にトランプのマーク(スペード、ハート、ダイヤモンド、クラブ)など、具体的なローバリューがない場合、C言語的な感覚でローバリューを設定する必要はありません。これは面白い例です。
Swiftの列挙型ではsimpleDescription
というメソッドがあり、スペードの場合には"Spades"
、ハートの場合には"Hearts"
のように文字列を返します。この方法なら、わざわざローバリューを設定する必要がなく、シンプルに記述できます。ただ、文字列でローバリューを設定する場合もあり、適切な方法については意見が分かれることがあります。
コード全体で見てシンプルに書ける方法として、CustomStringConvertible
プロトコルに準拠する方法があります。このプロトコルを使えば、description
プロパティを自分の列挙型に追加するだけで済みます。
最後に、渡辺さんがコメントしていたように、文字列に依存した処理はなるべく避けるべきです。ローバリューを使ってログを出す程度であれば問題ありませんが、重要な判定や分岐に使用するのは避けるべきです。これがプログラムの保守性を高めるためにも重要です。 ケース・バイ・ケースで皆さん、ハーフスペースがなくなったらコンパイルは通るけれども、実行時に予期しない動作になることがありますよね。現在の話でいえば、APIの仕様が変わった場合には、基本的にはケースでローバリューを指定せず、変わった場合にはイコールダブルクオーテーションで書くという感じです。あくまでもケース名はSwiftで使いやすい値にしておいて、たまたまAPIの名称とも一致していれば省略しますが、もしもAPI都合でスネークケースになったり、ケースが変更された場合には、仕様変更の観点で追加するという方法でよいでしょう。
このような事件がSwift3のときにありました。以前は大文字だったんですね。大文字だった場合、面倒ですよね。APIデザインガイドラインが導入された際、小文字にすべきとされ、多くの部分が崩壊しました。その結果、先頭大文字の "D" をダイヤモンドを書くことになりました。動画に依存しないというのは大事なことだと思います。
このようなことがあったため、仕様の変更が起こる可能性を考慮する必要があります。以前は列挙型の文字列化の表現が異なっていたり、型名も入っていたりしました。そのため、ロー型を指定しても動作が約束されていなかったという背景があります。現在も、その仕様が言語として搭載されているかは把握できていませんが、将来的に変わる可能性があるので、このあたりには慎重であるべきです。
そのため、列挙型の文字列化に対して恐怖感を抱くこともあります。また、コーダブルの標準的な書き方を省略したくなる場合もあります。標準のコーダブルの書き方は省略することが多いですが、ペースによっては表現を揃えるために書くこともあります。
恐怖感は微かにあるものの、現状では安定して動作しているため、省略ストリングを割り当てることも検討してみても良いでしょう。ただ、自分の方針としては、暗黙的なローバリューを避け、カスタムストリングコンバーティブルを使い、インスタンスを確実に表現できる文字列に変換するよう努めています。
この背景には、過去に列挙型の仕様変更が頻繁に繰り返されていたことがあります。省略ストリングの設定は、安定して動いているならばOKかなと思いますが、さらに複雑な文字列表現が必要な場合や、多くのケースが存在する場合には、どちらがローバリューなのか曖昧さが出ることもあるため、直接書く方が良いかもしれません。 とりあえず特定のローバリューを設定するのではなく、用途に応じてきちんと実装を書くのも一つの方法だと思います。無理に正当化しているような感じも拭えませんが、そのあたりは状況や個人の価値観に合わせて考えるべきではないでしょうか。とりあえず考えることを忘れなければ大丈夫だと思います。
暗黙的にストリングを盲目的に付けるのではなく、コードを書くたびに考えることが重要です。幾つかのコメントが寄せられていますが、例えば「XXXYYYだけならデコーディングストラテジー全体的に、スネークケースやキャピタルケースが統一されていれば問題はない」などの意見があります。
では、以上で終わりにしたいと思います。ありがとうございました。
さて、JSONエンコーダー・デコーダーなどのコーダブルのプロパティキーデコーディングストラテジーを使うと良いと思います。コーダブルを想定したストリングのローバリューについてですが、コーダブルは列挙型に意味を持たせるプロトコルなので、積極的に省略しても良さそうですね。
確かに、何も指定しないというのは言い過ぎかもしれませんが、列挙型を利用する仕組みは面白いです。特にC言語のようにいきなり異なる型に渡すことがない点は良いですね。
以前は列挙型の使用が恐怖でしたが、今ではコーダブルを活用して積極的に使っています。また、コーダブルのconvertFromSnakeCase
については、互換性がない場合が怖いので使わないという意見もあります。確かに、サーバーからのレスポンスが決まっている場合などは積極的に使うのも一つの方法かもしれません。
列挙型を使うことで、特定の範囲の値に制限できるので、それがローバリューを使う際の利点です。例えば、UUIDのような内部的な値として認識されるものは、適切に使うことができます。このような場合には心配なく使えます。
時間も良い具合になってきたので、話をこれで終えましょう。他に何か質問があればどうぞ。
それでは、今日はこれで終了とさせていただきます。お疲れ様でした。ありがとうございました。