effective-swift

Item 37. ordinal 인덱싱 대신 EnumMap을 사용하라

자세한 설명은 책에서 상세히 설명하고 있으므로 생략하겠습니다. 아래는 요약입니다.

참고로,

item37 본문 마지막에

실제 내부에서는 맵들의 맵이 배열들의 배열로 구현되니 낭비되는 공간과 시간도 거의 없이 명확하고 안전하고 유지보수하기 좋다.

라는 말이 나오는데 이 부분에 대해서는 이 링크를 참고하면 좋을 것 같습니다. (이번 주제와 연관성이 적어 직접 설명하지는 않겠습니다.)

Plant-LifeCycle 예시를 Swift 버전으로 바꾸기

해당 아이템의 코드 37-1 부터 코드 37-2 까지의 예시인 Plant-LifeCycle을 Swift 버전으로 바꾸어 보았습니다.

struct Plant: Hashable, Equatable {
    enum LifeCycle: Int, CaseIterable {
        case annual
        case perennial
        case biennial
    }
    
    let name: String
    let lifeCycle: LifeCycle
    
    init(name: String, lifeCycle: LifeCycle) {
        self.name = name
        self.lifeCycle = lifeCycle
    }
}

let garden: [Plant] = [Plant(name: "foo", lifeCycle: .annual),
                       Plant(name: "goo", lifeCycle: .perennial),
                       Plant(name: "koo", lifeCycle: .biennial)]
var plantsByLifeCycle: Array<Set<Plant>> = Array.init(repeating: Set<Plant>(), count: Plant.LifeCycle.allCases.count)

for plant in garden {
    plantsByLifeCycle[plant.lifeCycle.rawValue].insert(plant)
}

for i in 0 ..< plantsByLifeCycle.count {
    print("\(Plant.LifeCycle.allCases[i]): \(plantsByLifeCycle[i])")
}

// annual: [Plant(name: "foo", lifeCycle: Plant.LifeCycle.annual)]
// perennial: [Plant(name: "goo", lifeCycle: Plant.LifeCycle.perennial)]
// biennial: [Plant(name: "koo", lifeCycle: Plant.LifeCycle.biennial)]
let plantsByLifeCycle: [Plant.LifeCycle: Set<Plant>] = Plant.LifeCycle.allCases.reduce([Plant.LifeCycle: Set<Plant>]()) { (result, lifeCycle) -> [Plant.LifeCycle: Set<Plant>] in
    var result = result
    result[lifeCycle] = Set<Plant>(garden.filter{ $0.lifeCycle == lifeCycle })
    return result
}

print(plantsByLifeCycle)

// [Plant.LifeCycle.biennial: Set([Plant(name: "koo", lifeCycle: Plant.LifeCycle.biennial)]), Plant.LifeCycle.annual: Set([Plant(name: "foo", lifeCycle: Plant.LifeCycle.annual)]), Plant.LifeCycle.perennial: Set([Plant(name: "goo", lifeCycle: Plant.LifeCycle.perennial)])]

Phase-Transition 예시를 Swift 버전으로 바꾸기

해당 아이템의 코드 37-5 부터 코드 37-7 까지의 예시인 Phase-Transition을 Swift 버전으로 바꾸어 보았습니다. Transition을 Enum 타입이 아닌 Struct 타입으로 두고 CaseIterableEquatable 을 채택하게 해서 Swift 버전으로 구현할 수 있었습니다. 개인적으로 새로 알게 된 것은 Enum 타입뿐만 아니라 Struct, Class 타입도 CaseIterable 을 채택할 수 있다는 점입니다.

enum Phase: CaseIterable {
    case solid
    case liquid
    case gas
    
    struct Transition: CaseIterable, Equatable {
        public static var allCases: [Transition] {
            return [melt, freeze, boil, condense, sublime, deposit]
        }
        
        let from: Phase
        let to: Phase
        
        static let melt = Transition(from: .solid, to: .liquid)
        static let freeze = Transition(from: .liquid, to: .solid)
        static let boil = Transition(from: .liquid, to: .gas)
        static let condense = Transition(from: .gas, to: .liquid)
        static let sublime = Transition(from: .solid, to: .gas)
        static let deposit = Transition(from: .gas, to: .solid)
        
        private init(from: Phase, to: Phase) {
            self.from = from
            self.to = to
        }
        
        static let m: [Phase: [Phase: Transition]] = Transition.allCases.reduce([Phase: [Phase: Transition]]()) { (result, transition) -> [Phase: [Phase: Transition]] in
            var result = result
            
            if result[transition.from] == nil {
                result[transition.from] = [transition.to: transition]
                return result
            }
            
            result[transition.from]![transition.to] = transition
            return result
        }
        
        static func from(from: Phase, to: Phase) -> Transition? {
            guard m[from] != nil else { return nil }
            guard m[from]![to] != nil else { return nil }
            
            return m[from]![to]!
        }
    }
}

Swift에서 EnumMap 사용 예시

< Fonts Dictionary >

enum TextType {
    case title
    case subtitle
    case sectionTitle
    case body
    case comment
}

// fonts
let fonts: [TextType : UIFont] = [
    .title : .preferredFont(forTextStyle: .headline),
    .subtitle : .preferredFont(forTextStyle: .subheadline),
    .sectionTitle : .preferredFont(forTextStyle: .title2),
    .comment : .preferredFont(forTextStyle: .footnote)
]

위 Fonts Dictionary 예시에서 보면 .body에 대한 내용이 fonts에서 누락되었음을 컴파일러가 알려주지 않기 때문에 알아차리기 어려울 수 있습니다. 이렇게 일일이 Fonts Dictionary를 추가하는 것 보다 CaseItable를 사용하는 것이 더 낫습니다.

CaseItable

enum TextType: CaseIterable {
    case title
    case subtitle
    case sectionTitle
    case body
    case comment
}

var fonts = [TextType : UIFont]()

for type in TextType.allCases {
    switch type {
    case .title:
        fonts[type] = .preferredFont(forTextStyle: .headline)
    case .subtitle:
        fonts[type] = .preferredFont(forTextStyle: .subheadline)
    case .sectionTitle:
        fonts[type] = .preferredFont(forTextStyle: .title2)
    case .body:
        fonts[type] = .preferredFont(forTextStyle: .body)
    case .comment:
        fonts[type] = .preferredFont(forTextStyle: .footnote)
    }
}

이제 allCases를 사용하여 콜렉션(collection)의 모든 case에 순서대로 접근(access)할 수 있습니다. 그런 다음 case를 빠뜨릴 일 없이 Fonts Dictionary 을 만들 수 있습니다.

// 사용
let titleFont = fonts[.title] // titleFont의 타입은 UIFont? 입니다.

… we also have a bit of a problem at the call site.

Since we’re using a dictionary to store our fonts, even though we’re now using an exhaustive type for its keys, there’s no way for the compiler to guarantee that we’ll actually have a UIFont value for each key.

딕셔너리를 사용하여 글꼴을 저장하고 있기 때문에 현재 키에 대해 완전한 유형을 사용하고 있지만 컴파일러가 실제로 각 키에 대한 UIFont 값을 가질 것이라고 보장 할 방법이 없습니다.

하지만 여기서 더 개선할 수 있는 부분이 있습니다. 위 코드는 모든 case에서 똑같이 fonts[type]을 반복적으로 수행하고 있습니다. 게다가 optional 입니다.


EnumMap의 아이디어를 빌려 직접 구현하여 옵셔널과 반복되는 코드를 해결할 수있습니다.

EnumMap 구현

enum TextType {
    case title
    case subtitle
    case sectionTitle
    case body
    case comment
}

struct EnumMap<Enum: CaseIterable & Hashable, Value> {
    private let values: [Enum : Value]

    init(resolver: (Enum) -> Value) {
        var values = [Enum : Value]()

        for key in Enum.allCases {
            values[key] = resolver(key)
        }

        self.values = values
    }

    subscript(key: Enum) -> Value {
        // Here we have to force-unwrap, since there's no way
        // of telling the compiler that a value will always exist
        // for any given key. However, since it's kept private
        // it should be fine - and we can always add tests to
        // make sure things stay safe.
        return values[key]!
    }
}

let fonts = EnumMap<TextType, UIFont> { type in
    switch type {
    case .title:
        return .preferredFont(forTextStyle: .headline)
    case .subtitle:
        return .preferredFont(forTextStyle: .subheadline)
    case .sectionTitle:
        return .preferredFont(forTextStyle: .title2)
    case .body:
        return .preferredFont(forTextStyle: .body)
    case .comment:
        return .preferredFont(forTextStyle: .footnote)
    }
}

// 사용
let titleFont = fonts[.title]
let subtitleFont = fonts[.subtitle]

참고

  1. Enum iterations in Swift - Swift by Sundell