###1.はじめに
前回に列挙型
について解説しましたが、今回は事前にお伝えした通り、共用型の列挙型
について解説をしていこうと思います。
前回の記事と非常に深い関わりがあるので、まだご覧になっていない方はこちらの記事もご覧ください。
(https://qiita.com/0901_yasyun/items/a87c49438db68241778b)
###2.共用型の列挙型の概要について
共用型の列挙型
は、実体型
を指定しないシンプルな列挙型
と、複数の異なるタプル
の構造を併せ持つことができる型です。
この型にはいく通りかの異なるデータを、共通の概念でまとめて扱えるようにする狙いがあります。
まず、共用型の列挙型
の定義の概要を次に示します。
シンプルな列挙型
のケース名後ろにタプル
の宣言を付けた形になっています。
enum 型名 {
case ケース名 タプル型宣言
case ケース名 タプル型宣言
... ...
次に具体的な定義例を示します。
この例では、Webページなどの作成などで使用する色の指定は、文字列の色名、16進数によるカラーコードなどで行われますが、どの形式でも色を指定するという目的は同じなので、これを1つのデータ型で表すことを考えます。
enum WebColor {
case name(String) // 色の名前
case code(String) // 16進カラーコード
case white, black, red // よく使う色
}
この定義を使って、様々なインスタンスを生成できます。
let background = WebColor.name("indigo") // インディゴブルー
let turquoise: WebColor = .code("#40E0D0") // ターコイズ
let textColor = WebColor = .black // 黒
タプル型の構造に記述される情報を以下では付加情報
、またはペイロード
と呼びます。
共用型
の定義には、実体型
を記述することはできません。
値型
と共用型の列挙型
は別のものとして記述しなくてはなりません。
前回のシンプルな列挙型
、値型の列挙型
では要素同士を比較することができましたが、共用型の列挙型
の場合、自分で==演算子を定義するか、プロトコルEqutableを採用しない限り、相互に比較はできません。
if turquoise == WebColor.code("#40E0D0") // エラー。比較できない
もう少し複雑な例を示します。
次の例では、市営地下鉄で販売されている数種類の切符やプリペイドカードのどれもが、自動改札機で処理できるとし、切符やカードの種類を列挙型
で定義しました。
enum Ticket {
case 切符(Int, Bool, 回数券:Bool) // 普通券:価格、小人、回数券かどうか
case カード(Int, Bool) // プリペイドカード:残高、小人
case 敬老バス // 敬老バス
}
caseに記述する括弧の中はタプル
で、項目にキーワードを付けることもできます。
このような列挙型
に対する処理を書き分けるために、switch文
を使います。
caseで定数や変数へ値を代入でき、付加情報
を使わない場合には、下の「.カード」のように括弧以降を記述しないこともできます。
以下では、caseの後に置かれる構造に関する条件の記述をcaseパターン
と呼びます。
また、where以下に条件を書くことができるのはタプル
を扱う場合と同じです。
次の例を確認してください。
switch t {
case let .切符(fare, flag, _):
print("普通券: \(fare) " + (flag ? "小人" : "大人"))
case .敬老バス:
print("敬老バス")
case .カード(let r, true) where r < 110: // 小人の最低運賃
print("カード: 残高不足")
case .カード(let r, false) where r < 230: // 大人の最低運賃
print("カード: 残高不足")
case .カード:
print("カード")
タプル
をswitch文で使う場合と同様、付加情報
を定数に代入するにはletを()の中に置く方法と外側に置く方法が使えます。
case let .切符(fare, flag, _): // または
case .切符(let fare, let flag, _):
キーワード付きのタプルの場合、caseパターン内にはラベルを書かなくて大丈夫ですが、書くこともできます。
letとともに使う場合、letを書く場所に注意が必要です。
case let .切符(fare, _, 回数券: flag): // または
case .切符(let fare, _, 回数券: let flag):
###3.if-case文について
与えられたタプルが、複数の条件のどれに一致するかを調べるには、switch文が便利に利用できました。
しかし、特定の1つの条件のみに一致するかどうか調べたいという場合もあります。
例えば、先ほどの例で用いたTicket型の変数pが、プリペイドカードかどうかだけ調べたいとします。しかし、if文で次のようには記述できません。
if p == .カード { // エラー。この記述はできない
print("プリペイドカード")
}
switch文なら次のように記述できますが、使わないとわかっているdefault節も書かなければならず、少々面倒です。
switch p {
case .カード: print("プリペイドカード")
default: break
}
そこで、こういった場合でも容易に記述できる構文が用意されています。
上の例は次のように記述できます。この構文をif-case文
と呼ぶことがあります。
if case .カード = p {
print("プリペイドカード")
条件はまずcaseパターン
を記述し、「=」と対象となる式を置きます。
caseパターン
と式が一致するかどうかは、switch文の場合と同様に処理されます。
switch文の場合と異なり、付加的な条件はwhere節ではなく、カンマ「,」で区切って後ろに続けます。
この記法はif文、while文、およびguard文
の条件の書き方で説明したものと同じで、カンマで区切って、一般の式、オプショナル束縛構文
などを記述可能です。
if caseパターン = 式, 条件 {.....}
次の例では、変数tの内容が残額が1200円以上のプリペイドカードかどうかを調べます。
if case .カード(let y, _) = t, y >= 1200 { .....
少し複雑な例として、辞書のインスタンスからTicket型の要素を取り出し、その情報がパターンに一致しているかを調べるという条件を記述してみましょう。
let tickets:[String:Ticket] = [ "志倉" : .切符(260, false, 回数券: true),
"佐々木" : .切符(220, false, 回数券:false)]
let name = "佐々木"
if let t = tickets[name], case .切符(220, _, _) = t {
print("220円券")
}
この例の場合、辞書から得た情報がオプショナル型
なので、まずオプショナル束縛構文
で定数tにTicket型のインスタンスを得ます。
nilの場合はそこでifの条件が不成立となりますが、nilでなければ、次に定数tが220円の切符かどうか調べます。
なお、上のif文は「?」を使うと次のように記述することもできます。
if case .切符(220, _, _)? = tickets[name] {
print("220円券")
}
###4.for-in文でcaseパターンを使う
for-in文でも、caseパターン
を利用できます。
これまでに説明したfor-in文と同様に、配列などのSequenceプロトコル
に適合した式をinの次に置き、インスタンスを次々に取り出します。
この書き方では、取り出されたインスタンスとcaseの次のパターンが一致した場合にだけ、コードブロックを実行します。
実行のための条件をさらに記述する必要があれば、where節を追加することもできます。
for case パターン in 式 where 式 {
文... ...
}
たとえば、Ticket型の要素を持つ配列passesから次々にインスタンスを取り出して、220円より高い切符の情報を表示するには次のようにします。
for case let .切符(fare. child, coupon) in passes where fare > 220 {
var k = coupon ? "回数券" : "普通券"
if child { k += "(小人)" }
print(k, "\(fare)円")
}
この文は次のように記述するのと同じです。
for t in passes {
switch t {
case let .切符(fare, child, coupon):
if fare > 220 {
var k = coupon ? "回数券" : "普通券"
if child { k += "(小人)" }
print(k, "\(fare)円")
}
default: break
}
}
この構文でオプショナル型
に当てはまる「?」を利用すると、オプショナル型
が含まれるデータ列をうまく扱うことができます。
また、letではなく、varを使うこともできます。
###5.再帰的な列挙型
共用型の列挙型
で、タプルの内部に自分自身を要素として指定したい場合がありえます。
例えば次のような場合を考えてみます。
enum メッセージ {
case 文書(String, String) // 差出人、文書
case データ(String, [Int8]) // 差出人、データ列
case 転送(String, メッセージ) // 差出人、メッセージ
}
このデータの宣言は間違っていないようにも見えますが、Swiftでは扱うことができません。
Swiftでは列挙型
は値型
のデータですので、直感的に説明するならば、負荷情報は列挙型
のデータを表すメモリ位置に直接並べて格納されています。
上記のように自分自身を含む、つまり再帰的なデータ構造はデータを格納するために必要なメモリ容量がコンパイル時に決定することができません。
これに対して参照型
のデータは、メモリ上のどこかに存在する実体を間接的に参照するためのための情報なので、メモリの問題はありません。
しかし、再帰的な列挙型
が使えると便利な場面も多いので、Swiftでは列挙型
が自分自身を付加情報に含む場合、該当するcaseの前にindirectというキーワードを記述します。
これを間接指定
と呼びます。indirect
、つまり間接であるとは、その部分だけポインタのように間接参照を行うことを意味します。
今の話を以下の例に示します。
CustomStringConvertibleプロトコル
を採用し、printで表示できるようにしています。
enum メッセージ : CustomStringConvertible {
case 文書(String, String) // 差出人、文書
case データ(String, [Int8]) // 差出人、データ列
indirect case 転送(String, メッセージ) // 差出人、メッセージ
ver description: String {
switch self {
case let .文書(from, str): return from + "(" + str + ")"
case let .データ(from, _): return from + "[データ]"
case let .転送(from, msg): return from + "←\(msg)"
}
}
}
実行例を示します。
let m1 = メッセージ.文書("伊藤", "休みます")
let m2 = メッセージ.転送("白石", m1)
let m3 = メッセージ.転送("山田", m2)
print(m3) // "山田←白石←伊藤(休みます)"を出力
再帰的なcaseパターン
がいくつかある場合には、enumの前にindirect
を置いて全体に間接指定を行うことができます。
###6.おわりに
今回は共用型の列挙型についての記事を書きましたが、プロトコルやオプショナル型の扱いについては、記事が少し長くなってしまうので今回は解説していません。
来週以降はクラスなどについての解説をしていこうと思います。
ここまで読んでくれた方、ありがとうございました。