Swiftの列挙型(enum)おさらい

  • 179
    いいね
  • 0
    コメント

背景

Swiftの列挙型(enum)は他言語の列挙型と違い様々なことができます
しかし具体的にどんなことができるのか、きちんと調べたことが無かったのでこれを機に備忘録としてSwiftのenumができることをまとめてみました

これまでの列挙型に対するイメージ

  • 有限集合に名前をつけたもの
  • 有限集合内の各要素に名前をつけて可読性向上

Swiftの列挙型(enum)あれこれ

それではSwiftの列挙型でできることを紹介していきます

標準の列挙型

enum BloodType {
    case ab
    case a
    case b
    case o
}

let typeAB = BloodType.ab

標準的なenumです

型が分かっている場合はenum型を省略してlet typeAB: BloodType = .abと書けます

値型enum(raw value enum)

各要素に指定した型の値を割り当てることができます
各要素に割り当て可能なのは「整数値リテラル」、「浮動小数点数値リテラル」、「文字列リテラル」のみです
その他の型を割り当てようとするとエラーになります
値型enumはenumの宣言の後ろにクラスの継承と同じように: 割り当てたい型という書き方になります
もちろん、それぞれの要素の値の型は全て同じでないといけません

enum Signal: Int {
    case blue = 1
    case yellow = 2
    case red = 3
}

enum Weather: String {
    case sunny = "晴れ"
    case cloudy = "曇り"
    case rain = "雨"
    case snow = "雪"
}

//値型enumがサポートしていない型なのでこれはエラーになる
enum DeviceSize: CGSize {
    case iPhone4 = CGSize(width: 320, height: 480)
    case iPhone5 = CGSize(width: 320, height: 568)
}

そして値型enumはrawValueを使うと要素に割り当てられた値を取得することができます

let signalBlue: Signal = .blue
signalBlue.rawValue //1

let sunnyWeather: Weather = .sunny
sunnyWeather.rawValue //晴れ

また、値から列挙型を生成するイニシャライザが存在します
引数のrawValueに指定した値に対応する要素がない場合はnilが返ってきます

//返り値はSignal?
let signalYellow: Signal? = Signal(rawValue: 2)
signalYellow?.rawValue //2

//Signalには4に該当する要素が存在しないのでnilが返る
let signalGreen: Signal? = Signal(rawValue: 4)
signalGreen?.rawValue //nil

値型enumで要素に値を割り当てなかった場合、数値リテラルの場合は一番上の要素が0になり、下にいくつにれてインクリメントされた値になります
文字リテラルの場合は、要素と同じ名前の値になります

enum IntEnum: Int {
    case one
    case two
    case three
}

IntEnum.one.rawValue //0
IntEnum.two.rawValue //1
IntEnum.three.rawValue //2

enum BloodType: String {
    case ab
    case a
    case b
    case o
}

BloodType.ab.rawValue //"AB"
BloodType.a.rawValue //"A"
BloodType.b.rawValue //"B"
BloodType.o.rawValue //"O"

付属型enum(Associated Value)

値付きenumとか関連型enumとか呼ばれているかもしれませんが、ここでは付属型enumと表記します
(Associated Valueが1番しっくりくる方が多いのでしょうか)

付属型enumは要素に付属する値を定義することができます
要素の付属値は値型のようにあらかじめ決められた値で初期化されているものではありません
付属値は要素そのものの値ではないので値型のようにサポートできる型の制限もありません
また、各要素の付属値の数や型が違っていても構いません

enum Barcode {
    case upca(Int, Int, Int, Int)
    case qrcode(String)
}

var productBarcode = Barcode.upca(8, 85909, 51226, 3)
productBarcode = .qrcode("ABCDEFGHIJKLMNOP")

このようにバーコードでも規格が違えば必要になる情報が違うので、UPCAの付属情報とQRCodeの付属情報をそれぞれBarcode列挙型の要素(規格)ごとに定義できるので可読性が良くなります

もう1つの例として株の売買取引を想像してみてください
株の売買取引には「買い」と「売り」の2つの要素があります
そしてその要素には、それぞれ、どの「銘柄」を「いくら」といった付属情報が必要になります
これを「買い」と「売り」という要素だけ列挙型にして、注文処理をstructのメソッドで表現すると下記になるとします

struct StockTrade {

    enum Trade {
        case buy
        case sell
    }

    func order(tradeType: Trade, stock: String, amount: Int) {
        switch tradeType {
        case .buy:
            //買い注文
            print("\(stock)株を\(amount)購入する")
        case .sell:
            //売り注文
            print("\(stock)株を\(amount)売却する")
        }
    }

}

orderメソッドの引数として要素と株の銘柄と金額を受け取る必要があります
これよりも付属型enumを使用して株の売買取引に必要な情報を列挙型で完結させて、下記のように書いた方が可読性が良い気がします

struct StockTrade {

    enum Trade {
        case buy(stock: String, amount: Int)
        case sell(stock: String, amount: Int)
    }

    //注文処理 銘柄や金額はTradeに全て含まれているので引数はTradeのみ
    func order(type: Trade) {
        //買いもしくは売り注文を処理する
        switch type {
        case let Trade.buy(stock, amount):
            print("\(stock)株を\(amount)購入する")
        case let Trade.sell(stock, amount):
            print("\(stock)株を\(amount)売却する")
        }

    }

}

また、付属型enumの付属値はパターンマッチを使って取得することができます

enum Barcode {
    case upca(Int, Int, Int, Int)
    case qrcode(String)
}

let productBarcode = Barcode.upca(8, 85909, 51226, 3)
//パターンマッチ
if case let Barcode.upca(code) = productBarcode {
    print(code.0) //8
    print(code.1) //85909
    print(code.2) //51226
    print(code.3) //3
}

付属型enumにはラベルをつけてもつけなくても構いません

列挙型のネスト(Nested Types)

enumをネストして階層構造を明確にすることができます

例えばRPGにはキャラクター、武器、防具があります
武器や防具はキャラクターが装備するものなので、キャラクターを通してしかアクセスさせたくない時など、その関係性を階層構造で表すことができ、アクセスの方法も直感的なコードになります

enum Character {
    enum Weapon {
        case bow
        case sword
        case lance
        case dagger
    }
    enum Helmet {
        case wooden
        case iron
        case diamond
    }
    case thief
    case warrior
    case knight
}

//アクセス
let character = Character.thief
let weapon = Character.weapon.bow
let helmet = Character.helmet.iron

このように武器や防具の情報は、キャラクターを通してしかアクセスできません

また、構造体やクラスの中でもenumを宣言できます

struct Character {
    enum CharacterType {
        case thief
        case warrior
        case knight
    }
    enum Weapon {
        case bow
        case sword
        case lance
        case dagger
    }
    let type: CharacterType
    let weapon: Weapon
}

let warrior = Character(type: .warrior, weapon: .sword)

これも先の例と同じように、キャラクターは武器と防具を装備しているといったことが直感的に分かるコードだと思います
付属型enumを使って下記のように表現することも可能です

enum Character {
    enum Weapon {
        case bow
        case sword
        case lance
        case dagger
    }
    enum Helmet {
        case wooden
        case iron
        case diamond
    }

    case thief(weapon: Weapon, helmet: Helmet)
    case warrior(weapon: Weapon, helmet: Helmet)
    case knight(weapon: Weapon, helmet: Helmet)
}

let knight = Character.knight(weapon: .sword, helmet: .diamond)

列挙型のメソッドとプロパティの宣言

メソッド

Swiftの列挙型はメソッドを定義することができます

enum Wearable {
    enum Weight: Int {
        case light = 1
    }
    enum Armor: Int {
        case light = 2
    }
    case helmet(weight: Weight, armor: Armor)

    func attributes() -> (weight: Int, armor: Int) {
        switch self {
        case .helmet(let w, let a): return (weight: w.rawValue * 2, armor: w.rawValue * 4)
        }
    }
}

let woodenHelmetProps = Wearable.Helmet(weight: .Light, armor: .Light).attributes()
print(woodenHelmetProps) //(2, 4)

Swiftの列挙型の網羅チェックは、とても優れていて、switch文を使った時に要素を網羅していない場合はエラーになります

enum Fruit {
    case apple
    case banana
    case orange
    case melon

    func inJapaneseName() -> String {
        switch self {
        case .apple:
            return "りんご"
        case .banana:
            return "バナナ"
        case .orange:
            return "みかん"
        case .melon:
            return "メロン"
        }
    }
}

上記の列挙型にwaterMelon要素が増えた場合、switch文に.waterMelonの処理を書かないとエラーになります
要素の追加だけして処理を書き忘れる場合を想定して列挙型に対するswitch文ではdefault:を書かないほうが安全だと思います

また、タイプメソッドも定義できます

enum Device { 
    case appleWatch 
    static func fromSlang(term: String) -> Device? {
      if term == "iWatch" {
      return .appleWatch
      }
      return nil
    }
}
print(Device.fromSlang("iWatch"))

このようにタイプメソッドを使えば、値型enumのように値から列挙型を生成することができます
(カスタムのinitを実装する手もありますが)

また、mutating属性をメソッドに定義することで列挙型自身(self)を変更することができます

enum Direction: String {
    case north = "北"
    case east = "東"
    case south = "南"
    case west = "西"

    mutating func next() {
        switch self {
        case .north:
            self = .east
        case .east:
            self = .south
        case .south:
            self = .west
        case .west:
            self = .north
        }
    }
}

var direction = Direction.north
direction.next()
direction.rawValue //"東"

プロパティ

プロパティの宣言は算出型プロパティ (computed properties)とタイププロパティ (type properties)を定義することができます
格納型プロパティ (stored properties)は宣言できません

enum Device {
    case iPad, iPhone
    var year: Int {
        switch self {
        case iPhone: return 2007
        case iPad: return 2010
        }
    }

    static let operatingSystem = "iOS"
}

Device.iPad.year //2010
Device.operatingSystem //"iOS"

プロトコルの準拠とExtension

Swiftの列挙型はプロトコルに準拠できます

試しに、print関数で出力できる文字列を指定可能にするCustomStringConvertibleプロトコルに準拠させてみます
CustomStringConvertibleに準拠するにはvar description: String { get }を実装する必要があります

public protocol CustomStringConvertible {
    /// A textual representation of `self`.
    public var description: String { get }
}

列挙型では算出型プロパティ (computed properties)を宣言できるので下記のように簡単に準拠できます

enum Fruit: CustomStringConvertible {
    case apple
    case banana

    var description: String {
        switch self {
        case .apple:
            return "りんご"
        case .banana:
            return "バナナ"
        }
    }

}

print(Fruit.apple) //りんご

次に、銀行口座を管理する下記のようなプロトコルがあります

protocol AccountCompatible {
    var remainingFunds: Int { get }
    mutating func addFunds(amount: Int) throws
    mutating func removeFunds(amount: Int) throws
}

このプロトコルは、口座を管理する上で必要な「残高」のプロパティ、「入金処理」のメソッド、「出金処理」のメソッドを提供しています
このプロトコルに準拠して口座を管理するシステムの要件は下記になります

  • 入金・出金処理ともに残高から足し引きすれば良いが、残高より大きな金額を出金しようとすると「残高がいくら足りない」という例外を投げ、残高は例外が起きる前の残高になる

構造体で実現するのは下記のように簡単にできます

struct Account: AccountCompatible {

    var remainingFunds: Int = 0

    enum Error: ErrorType {
        case overdraft(amount: Int)
    }

    mutating func addFunds(amount: Int) throws {
        var newAmount = remainingFunds
        newAmount += amount
        if newAmount < 0 {
            throw Error.overdraft(amount: -newAmount)
        }
        else {
            remainingFunds = newAmount
        }
    }

    mutating func removeFunds(amount: Int) throws {
        try addFunds(amount * -1)
    }

}

var myAccount = Account(remainingFunds: 10000)
try? myAccount.addFunds(3000)
myAccount.remainingFunds //13000
try? myAccount.removeFunds(8000)
myAccount.remainingFunds //5000
do {
    try myAccount.removeFunds(13000)
}
catch Account.Error.overdraft(let amount) {
    print("残高が\(amount)円不足しています") //"残高が8000円不足しています"
}
print(myAccount.remainingFunds) //5000

このように構造体で対応するとinit時にremainingFundsプロパティ(残高)の値を設定して、入金・出金処理ではそれぞれremainingFundsプロパティから足し引きして実現しています

では、格納型プロパティ (stored properties)を宣言できない列挙型では、どうやって準拠させれば良いでしょうか

焦点となるのは残高のプロパティをどうやって表現すれば良いかですね

答えは付属型enum(Associated Value)を使えば解決することができます

enum Account: AccountCompatible {
    case empty
    case funds(remaining: Int)

    enum Error: ErrorType {
        case overdraft(amount: Int)
    }

    var remainingFunds: Int {
        switch self {
        case empty: return 0
        case funds(let remaining): return remaining
        }
    }

    mutating func addFunds(amount: Int) throws {
        var newAmount = amount
        if case let .funds(remaining) = self {
            newAmount += remaining
        }
        if newAmount < 0 {
            throw Error.overdraft(amount: -newAmount)
        } else if newAmount == 0 {
            self = .empty
        } else {
            self = .funds(remaining: newAmount)
        }
    }

    mutating func removeFunds(amount: Int) throws {
        try self.addFunds(amount * -1)
    }

}

var myAccount = Account.Funds(remaining: 10000)
try? myAccount.addFunds(3000)
myAccount.remainingFunds //13000
try? myAccount.removeFunds(8000)
myAccount.remainingFunds //5000
do {
    try myAccount.removeFunds(13000)
}
catch Account.Error.Overdraft(let amount) {
    print("残高が\(amount)円不足しています") //"残高が8000円不足しています"
}
myAccount.remainingFunds //5000

また、列挙型にもextensionが使えるので

enum Account {
    case empty
    case funds(remaining: Int)

    enum Error: ErrorType {
        case Overdraft(amount: Int)
    }

    var remainingFunds: Int {
        switch self {
        case empty: return 0
        case funds(let remaining): return remaining
        }
    }
}

extension Account: AccountCompatible {

    mutating func addFunds(amount: Int) throws {
        var newAmount = amount
        if case let .funds(remaining) = self {
            newAmount += remaining
        }
        if newAmount < 0 {
            throw Error.Overdraft(amount: -newAmount)
        } else if newAmount == 0 {
            self = .empty
        } else {
            self = .funds(remaining: newAmount)
        }
    }

    mutating func removeFunds(amount: Int) throws {
        try self.addFunds(amount * -1)
    }

}

と書くことができます
extensionを使って要素とメソッドを分けたりすることで可読性が上がりますよね
そして、列挙型で残高が0の時を表すEmptyも定義しているので、その口座の状態も簡単に知ることができます

また、コード内ではネスト型のenumであるError列挙型も定義しています
これはErrorTypeプロトコルに準拠しており、エラーの内容が有限でパターンが分かっている場合は、エラーのパターンごとに、必要となる付属情報を格納しておくことでエラーがとても扱いやすいものになります
もしエラーとなる要件が増えた場合もError列挙型に要素を増やして管理することができます

ジェネリクスを使った列挙型(Generic enum)

ジェネリクスを使うことができます
ジェネリクスを使った列挙型の最たる例はOptional型でしょうか

public enum Optional<Wrapped> : NilLiteralConvertible {
  case none
  case some(Wrapped)
  ....
}

このようにオプショナルはジェネリクスを使った付属型enum(Associated Value)で表現されています
また、「どちらか」を表す型のEitherも同じように表現できます

enum Either<T1, T2> {
    case Left(T1)
    case Right(T2)
}

ジェネリクスを使うと更に柔軟性が増しますね

列挙子(要素)なしのenum

要素のない列挙型の定義ができます
要素のない列挙型をどういったケースで使えるかというと、色情報を扱う定数の集合を下記のように構造体などのタイププロパティで宣言して使うことってありませんか?

struct ColorUtil {
    static let myRedColor = UIColor(red: 255.0/255, green: 69.0/255, blue: 0.0/255, alpha: 1.0)
    static let myGreenColor = UIColor(red: 0.0/255, green: 255.0/255, blue: 127.0/255, alpha: 1.0)
    static let myBlueColor = UIColor(red: 65.0/255, green: 105.0/255, blue: 225.0/255, alpha: 1.0)
}

このように構造体で色情報の集まりを生成した時に、使う側は常にタイププロパティにアクセスするので、イニシャライズする必要はありません
しかし特に何もしないと構造体の性質として、普通にイニシャライズが提供されてしまいます

//こういったアクセスしかしないのに
ColorUtil.myRedColor
ColorUtil.myGreenColor
ColorUtil.myBlueColor

//意味のないインスタンスをつくれてしまう
let colorUtil = ColorUtil()

これをenumの要素なしの、タイププロパティに書き換えてみると

enum ColorUtil {
    static let myRedColor = UIColor(red: 255.0/255, green: 69.0/255, blue: 0.0/255, alpha: 1.0)
    static let myGreenColor = UIColor(red: 0.0/255, green: 255.0/255, blue: 127.0/255, alpha: 1.0)
    static let myBlueColor = UIColor(red: 65.0/255, green: 105.0/255, blue: 225.0/255, alpha: 1.0)
}

//構造体と同じように使える
ColorUtil.myRedColor
ColorUtil.myGreenColor
ColorUtil.myBlueColor

//エラーになる
let colorUtil = ColorUtil()

このようになり、列挙型のイニシャライズを呼ぼうとするとエラーになります
色情報を使う側からしてもエラーが出てくれるので親切な設計になります

まとめ

いかがでしたか
このとおりSwiftの列挙型は本当に様々なことができますね

できることが多いゆえにstructやclassではなく、enumを選択して失敗したという経験をお持ちの方もいるかと思います

間違ったパターンでenumを選択してしまわないように一応私が心がけているのは、「これを有限集合の要素として扱うことが適しているか」ということを一度考えてからenumを選択するか決めるようにしています
(当然といえば当然ですが)

使い方次第では可読性が向上し、良いコードが書けるのではないでしょうか
(自身がenumの良い使い方をできているかどうかは微妙ではありますが(;^_^

Swiftのenumは列挙型の範疇を超えているから基本的に使わないという方もいるかもしれませんが、様々なベストプラクティスを模索するのも楽しいかもしれません

もしよければこの記事を参考にして使ってみてください

参考資料

ほぼ下記で勉強させてもらいました
Advanced & Practical Enum usage in Swift
Swift: The Case of An Enum With No Cases