💡

初学者の頃に知っておきたかった Swift の列挙型 Tips

2024/12/05に公開

はじめに

Swift の enum(列挙型)は柔軟性があり非常に強力で、私が Swift の中で最も好きな機能の一つです。そんな enum について、初学者の頃にぜひ知っておきたかったことをいくつか紹介します。

名前空間としての case 無し enum

case の無い enum はインスタンス化やオーバーライドができません。

enum CaselessEnum { }

CaselessEnum()    // 🚫  'CaselessEnum' cannot be constructed because it has no accessible initializers

この性質を利用して、静的なメソッドやプロパティのグループを定義するためによく使われます。

enum Constant {
    enum API {
        static let baseURLString: String = "https://example.com"
    }
}

Constant.API.baseURLString    // "https://example.com"

enum FooAPIClient: APIClientProtocol {
    static func fetchValue(for id: Foo.ID) async throws -> Foo {
        ...
    }

    static func fetchAllValues() async throws -> [Foo] {
        ...
    }
}

実際、Apple の API にもそのようなユースケースがあります。

https://developer.apple.com/documentation/combine/publishers

A namespace for types that serve as publishers.

case 無し enum を使うと、グループ化・構造化のための名前空間を意図したものであることが明確になります。

予約語での case 命名

特定のキーワードを case 名として使おうとすると、型チェッカーがコードを解析できないためエラーとなります。

enum Setting {
    case custom
    case default    // 🚫  if this name is unavoidable, use backticks to escape it
}

エラーを回避するためにはキーワードをバッククォートで囲みます [1]

enum Setting {
    case custom
    case `default`    // 👌
}

うれしいことに、呼び出すときにはバッククォートは要りません。

Setting.default    // default

CaseIterable プロトコル

CaseIterable プロトコルに準拠することで、型の allCases プロパティを使用して、すべての case のコレクションにアクセスできます。

enum CompassDirection: CaseIterable {
    case north, south, east, west
}

CompassDirection.allCases.count    // 4

CompassDirection.allCases.map({ "\($0)" }).joined(separator: ", ")    // "north, south, east, west"

リスト UI のアイテムを enum で表現する場面などにおいては、表示したいすべてのアイテムをかんたんに取得できます。 Associated Values なしの enum に限られるという制約はあるものの大変便利です。

また CaseIterable プロトコルはテストの文脈においても有用です。

enum に case を追加したのに、それに対応するテストケースを追加し忘れてしまう(そしてテストは成功してしまう👻)ことなどはよくあるのではないでしょうか?こんな場合にも enum をCaseIterable プロトコルに準拠させておけば、テストの網羅性を保証することができます [2]

enum Animal: CaseIterable {
    case cat
    case dog

    var sound: String {
        switch self {
        case .cat:
            return "Meow!"
        case .dog:
            return "Woof!"
        }
    }
}
import XCTest

final class AnimalTests: XCTestCase {
    func test_sound() {
        let testCases: [(line: UInt, animal: Animal, expected: String)] = [
            (#line, .cat, "Meow!"),
            (#line, .dog, "Woof!")
        ]

        // テストケースの網羅性をチェック ✅️
        Animal.allCases.forEach { animal in
            XCTAssert(testCases.contains(where: { $0.animal == animal }), "\(animal) がテストされていません!")
        }

        for testCase in testCases {
            XCTAssertEqual(testCase.animal.sound, testCase.expected, line: testCase.line)
        }
    }
}

まとめ

以上、Swift の enum に関する Tips でした。初学者の方の参考になれば幸いです。

https://qiita.com/advent-calendar/2024/gmo-media

脚注
  1. バッククォートによる予約後のエスケープは enum の case に限ったものではありません ↩︎

  2. だたしテストの都合のみでメインプログラムの実装を変えるのは微妙な気はします ↩︎

GMOメディアテックブログ

Discussion