列挙型(enum)について
履歴
2022年1月13日 初版
2023年9月13日 if let case について追記、名前空間利用についてもちょっと追記
開発環境
Swift5.5以降
Xcode13
概要
列挙型とは、識別子の集合である。 識別子を列挙した型。みたいな。
識別子は、何かを区別する際に用いるもので、例えば、トランプのカードの絵柄は「ハート」「ダイヤ」「クラブ」「スペード」の4種類であり、 カードの番号…ランクというらしい…、ランク(1〜13)もまた、カードを識別するために用いる識別子となる。
従って、トランプは「絵柄」と「番号」といったカードを区別をするための識別子をもって、一意に特定できることとなる。
カードの絵柄はジョーカーを除けば「ハート」「ダイヤ」「クラブ」「スペード」の4種類しかない。この4種類の絵柄の集合が、「トランプの絵柄」と言い換えることができる。
以下、ジョーカーを除いて考えるとして、
トランプのカードが絵柄を持つ時、次のPlayingCardSuitのいずれかが該当することになる。
列挙型は、enumで始まり名前を記述する。クラスや構造体同様、列挙型の命名は大文字から始まるアッパーキャメルケースである。
enum PlayingCardSuit {
case heart
case diamond
case club
case spade
}
カードの構造体を作る場合は、次のようになる。
struct PlayingCard {
// カードの絵柄
let suit: PlayingCardSuit
// カードの番号
let rank: Int
}
スペードの4を示す時、インスタンス生成はPlayingCardSuitのspade指定する。
指定の方法は、列挙型名.識別子である。
let spade4 = PlayingCard(suit: PlayingCardKind.spade, rank: 4)
利点
列挙型を型として利用する場合、列挙された識別子以外の値は存在しない。
PlayingCardとして指定されたsuitは、必ず4種類のうちのどれかに該当する。
Swift言語はswitchを利用する時、その型が取りうる値すべてを網羅していない限りdefault句が必要となる。
もし、カードの種類を「1はハート」「2はダイヤ」「3はクラブ」「4はスペード」とInt型でマッピングしたとしても、switch文は1〜4以外のdefaultも考慮しなければならない。
例として、PlayingCardのsuitがInt型であった時のswitch文を記す。
switch spade4.suite {
case 1:
print("♥")
case 2:
print("♦")
case 3:
print("♣")
case 4:
print("♠")
default:
print("?")
}
すべての値を網羅していない状態でdefault句がない場合はコンパイルエラーとなるので、必ず書かなければならない。
ただ、この場合はカードの種類(4種類しかない)から考えればdefaultは存在しないはずである。しかし、文法上は記述をしなければエラーになる。
もちろん、suiteがInt型である以上、1〜4以外の値が入らないとも限らない。
カードの設定としては不適切な値が許容されてしまうため、数値を使ったカードの種類のマッピングは避けるべきと考える。
しかし、列挙型を利用している場合は、列挙型のすべてを取り上げていれば、defaultが必要にならない。
そして、列挙型である以上、この4種類以外の値は取り得ることがない。
switch spade4.suite {
case PlayingCardSuit.heart:
return "♥"
case PlayingCardSuit.diamond:
return "♦"
case PlayingCardSuit.club:
return "♣"
case PlayingCardSuit.spade:
return "♠"
}
省略可能なケース
列挙型として指定しているプロパティ、あるいは列挙型を引数としている関数などは、該当する値は指定した列挙型であることが確定している。
例えば、先のカードの構造体のkindは必ずPlayingCardSuitのいずれかが入ることがわかっている。
struct PlayingCard {
let suite: PlayingCardSuit // ← heart、diamond、club、spadeのいずれか
let rank: Int
}
このカードのsuiteに値を代入する時、列挙型名を省略することができる。
let spade4 = PlayingCard(suite: .spade, rank: 4)
もちろん、switch文でもspade4のsuiteはPlayingCardSuitであることがわかっているため、case句のPlayingCardSuitは省略することができる。
switch spade4.suite {
case .heart:
return "♥"
case .diamond:
return "♦"
case .club:
return "♣"
case .spade:
return "♠"
}
ただし、型が定まっていない場合は型を省略することはできないので注意をする。
列挙型の反復(for文で使いたい)
列挙型で、内部の識別子をfor文で利用した時には、そのenumをCaseIterable
に準拠するようにする。
enumは、クラスや構造体同様、他のプロトコルに準拠させる記述も可能である。
CaseIterable
に準拠されることで、配列などで使うfor文が利用できるようになる。
enum PlayingCardSuit: CaseIterable {
case heart
case diamond
case club
case spade
}
print(PlayingCardSuit.allCases.count) // 4
for suite in PlayingCardSuit.allCases {
print(suite) // heart、diamond、club、spadeの順に表示される
}
rawValue
列挙型の識別子には識別子に値を持たせることができる。これをrawValueと呼ぶ。生の値。
識別子に値を持たせるものであり、識別子を別の値で利用するための手段と思って良い。つまり、代入可能な値ではない。
例えば先程のカードの種類を数値で表す……すなわち「1はハート」「2はダイヤ」「3はクラブ」「4はスペード」のようなケースを作成する場合、rawValueを使う。
rawValueを指定する場合は、列挙体名: の後ろに指定したい型名を書く。rawValueはCaseIterableと並行して記述しても問題はない。
enum PlayingCardSuit: Int, CaseIterable {
case heart = 1
case diamond = 2
case club = 3
case spade = 4
}
var spade6 = PlayingCard(suite: .spade, rank: 6)
print(spade6.suite.rawValue) //4 が表示される
rawValueはget(値の取得)は可能だがset(値の再代入)はできない。以下のようにrawValueを設定することはできない。
※suiteを列挙型で変更することはできる。rawValueの変更ができない。
spade6.suite = PlayingCardSuit.heart // 可能
spade6.suite.rawValue = 2 // 不可能
インスタンスを作る時にrawValueを用いて識別子を定めることができる。
ただし、rawValueで識別子を定めた列挙型を作ろうとする時、オプショナルになることに注意する。
(もし、PlayingCardKind(rawValue: 99999)と指定した場合、存在しない識別子を指定することになる)
let heartKind: PlayingCardKind? = PlayingCardKind(rawValue: 1)
if let heartKind = heartKind {
let heart10 = PlayingCard(kind: heartKind, rank: 10)
print(heart10.kind)
}
※rawValueは、識別子に対応する値であり、同じ列挙型内の他の識別子と同じrawValueを持たせることはできない。
rawValueで指定できる型
rawValueで指定できる型はString、Character、Int、Float、Doubleである。
それ以外の型はエラーとなる。
// DoubleもFloatもどちらでもOK
enum PlayingCardKind: Double, CaseIterable {
case heart = 0.0
case diamond = 1.0
case club = 2.0
case spade = 3.0
}
Associated Values
列挙型の識別子に関連した値(Associated Values)を持たせることもできる。
これまでのPlayingCardは内部にkindとrankをもたせた構造体で考えていたが、Associated Valuesを使うことで列挙型だけで管理することもできる。
代入ではなく、その識別子に関連した値を設定することができる。
Associated Valuesを使うには、列挙型の定義時に、Associated Valuesの型を丸括弧で括って指定する必要がある。
case 識別子(型)
のように書き、複数ある場合はcase 識別子(型, 型, 型)
とカンマ区切りで書いていく。
実際に、PlayingCardSuiteにrankの値が関連付けられるようにしてみる。
enum PlayingCardSuit {
case heart(Int)
case diamond(Int)
case club(Int)
case spade(Int)
}
変更に伴って、PlayingCardの情報も変更する。
「ダイヤの10」のカードを作ろうとする時、次のように記述する。
struct PlayingCard {
let card: PlayingCardSuit
}
let diamond10 = PlayingCard(card: PlayingCardSuit.diamond(10))
Associated Valuesは、switch文で分岐した時などに取得することができる。
つまり、switch文のcase句等で、定数として取り出すことで、利用ができるものである。値の代替となるプロパティのようには扱えない。
switch diamond10.card {
case .heart(let rank):
print("♥の\(rank)です")
case .diamond(let rank):
print("♦の\(rank)です")
case .club(let rank):
print("♣の\(rank)です")
case .spade(let rank):
print("♠の\(rank)です")
}
プロパティのように扱えない理由は、識別子ごとに関連する値の型、数をが指定できるためであるといえる。
つまり、共通化ができないものであるので、switch等で分岐してそれぞれの識別子ごとに値を取り出すことしかできない。
例えば、Joker(赤と黒の2枚)を種類として追加すると、列挙型は次のようになる。※enumの中にenumをネストことはできる。
enum PlayingCardSuit {
case heart(Int)
case diamond(Int)
case club(Int)
case spade(Int)
case joker(JokerKind)
enum JokerKind {
case red
case black
}
}
Jokerの赤を作るには次のような文になる。
let jokerRed = PlayingCard(card: PlayingCardSuit.joker(.red))
カードの種類がjokerの時は、値に「jokerの赤かjokerの黒」のどちらかの値が入っている。
ハート、ダイヤ、クラブ、スペードの時は値は「Int型」である。
case句の定数に入る値の型がjokerの時だけ異なることに注意する。
switch jokerRed.card {
case .heart(let rank):
print("♥の\(rank)です")
case .diamond(let rank):
print("♦の\(rank)です")
case .club(let rank):
print("♣の\(rank)です")
case .spade(let rank):
print("♠の\(rank)です")
case .joker(let jokerKind):
switch jokerKind {
case .red:
print("ジョーカーの赤です")
case .black:
print("ジョーカーの黒です")
}
}
もしも、全件の網羅をすること無く、jokerの情報を取得したい場合はif let caseを使うこともできる。
let joker = PlayingCardSuit.joker(.red)
if case let .joker(color) = joker {
// .jokerの時、clorを取得できる
print(color)
}
rawValueとAssociated Valuesは、同時に利用することができない。
また、Associated Valuesが設定されている場合は、CaseIterableも使えなくなる。
次の2つの列挙型は、両方ともエラーになるので注意をすること。
// エラーになる
enum Hoge: CaseIterable {
case fuga(Int)
case piyo(Int)
}
// エラーになる
enum Hoge: String {
case fuga(Int) = "FUGA"
case piyo(Int) = "HOGE"
}
rawValueとAssociated Valuesの違いは、rawValueはenumで定義した値が常に利用されるということであり、
Associated Valuesはインスタンスを作る際に異なる値を設定することができるという違いがある。
つまり、カードのランクのようにカードによって値が異なるのであればAssociated Valuesであるし、
ジョーカーのredとblackはどのような場合でもredは赤でありblackは黒であるという状況であればredとblackに対応する値はrawValueで定義することになる。
変数、関数の追加
列挙型は、非staticなストアドプロパティは定義することができないがコンピューテッドプロパティは定義することができる。
また、関数(メソッド)や比較演算子の定義も可能である。
staticなストアドプロパティは定義できる
次のPlayingSuiteは、Equatableに準拠させ、構造体同士で同じか異なるかの比較を可能とした。
絵柄を取得するため、コンピューテッドプロパティとしてsuiteを定義し、
ランクを取得するために、同じくコンピューテッドプロパティでrankを定義した。
変数suiteでは、処理内部のswitch文にあるcase句に定数を定義していないが、Associated Valuesが不要であれば(_)としてもよいし、書かないことも可能である。
enum PlayingCardSuit: Equatable {
// Equatableの「等しい」の条件
static func == (lhs: PlayingCardSuit, rhs: PlayingCardSuit) -> Bool {
return lhs.suite == rhs.suite
}
case heart(Int)
case diamond(Int)
case club(Int)
case spade(Int)
case joker(JokerKind)
// redは赤の意味しかなく、blackは黒の意味しかない。
enum JokerKind: String {
case red = "赤"
case black = "黒"
}
var suite: String {
switch self {
case .heart:
return "♥"
case .diamond:
return "♦"
case .club:
return "♣"
case .spade:
return "♠"
case .joker:
return "ジョーカー"
}
}
var rank: String {
switch self {
case .heart(let rank), .diamond(let rank), .club(let rank), .spade(let rank):
return String(rank)
case .joker(let jokerKind):
return jokerKind.rawValue
}
}
// メソッドの定義
func printMessage() {
switch self {
case .heart, .diamond, .club, .spade:
print("絵柄は\(self.suite)で、ランクは\(self.rank)です")
case .joker:
print("絵柄は\(self.suite)で、色は\(self.rank)です")
}
}
}
使用例は次のようになる。
struct PlayingCard {
let card: PlayingCardSuit
}
let diamond10 = PlayingCard(card: PlayingCardSuit.diamond(10))
let jokerRed = PlayingCard(card: PlayingCardSuit.joker(.red))
diamond10.card.printMessage() // 「絵柄は♦で、ランクは10です」と表示される
jokerRed.card.printMessage() // 「絵柄はジョーカーで、色は赤です」と表示される
// Equatableによる比較
print(diamond10.card == jokerRed.card ? "同じ絵柄です" : "異なる絵柄です") // 「異なる絵柄です」と表示される
実際のコードでよく使う、Errorに準拠するEnum
Swift言語ではErrorに準拠した値を受け取る型がしばしば登場する。
Errorはプロトコルであるため、そのまま利用することができず、Errorに準拠した型を自分で定義することが多い。
enum SampleError: Error {
case hogehogeError(String)
case fugafugaError(Int, String)
var message: String {
switch self {
case .hogehogeError(let message):
return message
case .fugafugaError(let status, let message):
return message + ":" + String(status)
}
}
}
let error1 = SampleError.hogehogeError("エラーメッセージ")
print(error1.message) //「エラーメッセージ」と表示される
let error2 = SampleError.fugafugaError(10, "エラー番号付き")
print(error2.message) // 「エラー番号付き:10」と表示される
Resultで使う
列挙型のResult型は、成功時はsuccess識別子にSuccess型の値をAssociated Valuesに詰め込み、
失敗時はfailure識別子にErrorに準拠した型の値をAssociated Valuesに詰め込むことができる列挙型である。
failureに設定できるのはErrorに準拠していなければならないため、先程のSampleErrorのようなErrorに準拠した型を作っておくと良い。
処理に成功した時はsuccessを使い、処理に失敗した時はfailureと、その失敗の理由を列挙型の識別子などで定義しておくと、エラーの処理が扱いやすくなる。
let error1 = SampleError.hogehogeError("エラーメッセージ")
let error2 = SampleError.fugafugaError(10, "エラー番号付き")
// 様々なResultの作成
let result1: Result<String, SampleError> = .failure(error1)
let result2: Result<String, SampleError> = .failure(error2)
let result3: Result<String, SampleError> = .success("成功")
switch result1 { //「エラーメッセージ」と表示される
case .success(let message):
print(message)
case .failure(let error):
print(error.message)
}
switch result2 { // 「エラー番号付き:10」と表示される
case .success(let message):
print(message)
case .failure(let error):
print(error.message)
}
switch result3 { // 「成功」と表示される
case .success(let message):
print(message)
case .failure(let error):
print(error.message)
}
名前空間として利用する
staticなメソッド、staticなプロパティを用いて、名前空間としてのenum利用もできる。
所感
enumはわかるととても便利。