2021년 1월 1일 금요일

Enum을 효과적으로 사용

 

Enum을 효과적으로 사용

열거 형을 적절히 사용하여 데이터를 모델링하는 방법

Swift는 흥미로운 언어입니다. 새로운 개발자에게 매우 친숙합니다. 구문은 간결하고 이해하기 쉬우 며 주요 개념도 있습니다.

그러나 실제로 마스터하려면 많은 시간이 걸립니다. 언어의 모든 기능에는 swift.org의 언어 가이드에 설명 된 것보다 더 깊은 세부 정보가 있습니다 .

오늘 기사에서는 enums의 고급 사용법을 살펴 보겠습니다 . Enums는 매우 강력하며 종종 제대로 사용되지 않거나 잘못 사용됩니다. 생각하지 못했던 사용법에 대해 알려 드리고자합니다.

모델 독점

가장 간단한 아이디어로 시작하여 enum대안을 표현하는 데 사용할 수 있습니다. runningcycling및 swimming? 중에서 선택해야합니다 . 를 사용 enum! 요일을 나열해야합니까? 를 사용 enum!

가장 간단한 경우입니다. 그러나 Swift의 열거 형은 그보다 더 강력합니다. Enums support associated values, 즉 특정 인스턴스와 관련된 데이터입니다. 예를 들어, running사례에는 수행 된 단계 수가 포함될 수 있고, cycling사례에는 운동 중에 사용 된 장비가 포함될 swimming수 있으며 세션은 수온의 영향을받을 수 있습니다.

모든 것을 합치면 다음과 같이 끝납니다 enum.

enum Workout {
case running(steps: Int)
case cycling(gear: Int)
case swimming(temperature: Double)
}
view rawenum.swift hosted with ❤ by GitHub

구분이 추상화하기가 그렇게 간단하지 않은 경우가 많이 있습니다. 어떤 경우에는 특정 유형의 분류를 기반으로 일부 속성을 가질 수 있거나 가질 수없는 실제 데이터를 모델링하려고합니다.

예를 들어 체중 운동을합시다. 스쿼트, 팔 굽혀 펴기, 판자, 철 의자가 있습니다. 운동은 다양한 일련의 운동으로 구성됩니다. 누군가 struct는 다음과 같은 방식으로 시리즈를 모델링하고 싶을 수 있습니다 .

struct Series {
let exerciseId: String
let repetitionCount: Int?
let duration: TimeInterval?
}
view rawseries.swift hosted with ❤ by GitHub

그러나 실제로 말이되지 않는 몇 가지 속성이 있습니다. 또는 예를 들어, repetitionCount판자 시리즈에는 의미가 없습니다. 이것이 우리가 선택 사항으로 표시해야하는 이유입니다. 동시에 duration스쿼트에 대해 이해가 될 수도 있고 그렇지 않을 수도 있습니다. 서킷을하고 있다면 한정된 횟수의 반복을 원할 수 있습니다. 하지만 타바타와 함께 운동을한다면 한정된 시간에 가능한 한 스쿼트를하고 싶을 것입니다.

따라서 다음과 같은 방식으로 모델을 리팩토링 할 수 있습니다.

enum Series {
case repetitionBased(exerciseId: String, count: Int)
case timeBased(exerciseId: String, duration: TimeInterval)
}
view rawseriesEnum.swift hosted with ❤ by GitHub

팁 : 공유 정보에 대한 유형 무시

exerciseId시리즈를 특정 연습과 연결하기 위해 모든 케이스가 동일한 필드를 갖는 방법에 주목하십시오 . 앱을 개발하는 동안 특정 유형의 운동에는 관심이 없지만 일반적인 정보에는 관심이있는 여러 곳에서 발생할 수 있습니다. 에 표현 된 유형을 우회하는 계산 된 변수를 만들 수 있습니다 enum.

extension Series {
var exerciseId: String {
switch self {
case .repetitionBased(let exerciseId, _):
return exerciseId
case .timeBased(let exerciseId, _):
return exerciseId
}
}
}

이 접근 방식은 유형 정의 내에서 공통 속성 추출을 그룹화하는 데 매우 유용하므로 호출 사이트가 깨끗하고 읽기 쉽게 유지됩니다.

Apple의 예 : Foundation의 결과 유형

Apple은 API에서도 이러한 추론 방식을 미묘하게 도입하고 있습니다. Apple 이 서버에 접속하기 위해 제공 한 첫 번째 API 에는 (Data?, URLResponse?, Error?)매개 변수가 있는 콜백이 필요했습니다 .

네트워크 호출이 성공하면 Data유형이 채워졌지만 Error그렇지 않은 것입니다. Viceversa, 오류 발생시 Error유형이 채워졌습니다. 문서에 두 경우 중 하나가 절대라고 명시되어 있지 않지만 원칙적으로 nil두 필드를 모두로 설정할 수 있습니다 .. 호출이 성공했지만 데이터 나 오류가 없습니다!nil201 — No Content


이런 일이 발생하지 않도록하고 데이터 나 오류가 nil이 아닌 코드에서 직접 모델링하기 위해 Apple은 라는 새로운 데이터 유형을 사용하기 시작했습니다 Result. 내부적으로 Result는 두 가지 상태가 될 수있는 열거 형일뿐입니다. success또는 failure. 의 경우 success요청의 내용 (이전 data. 의 경우 failure관련 값은 error모든 관련 정보와 함께입니다.

다형성

열거 형은 컬렉션에서 동종 유형의 제한을 우회하는 데 사용할 수 있습니다. Swift에서 컬렉션은 동일한 유형의 요소 만 포함 할 수 있습니다. a Set<Int>를 정의하면 그 안에 정수만 저장할 수 있습니다. 사용자가 사진 위에 텍스트와 스티커를 추가 할 수있는 사진 편집 앱을 고려해 보겠습니다.

Text그리고 Stickers매우 다른 행동과 특징을 가지고 있습니다. 우리는 그것들을 다른 구조체로 모델링하고 싶을 수 있습니다.

struct Text {
let content: String
let font: UIFont
let position: CGPoint
}
struct Stricker {
let fileURL: URL
let position: CGPoint
let scale: CGFloat
}

짐작 하시겠지만 enum. 에는 위에서 정의한 동일한 구조 인 자체 관련 값 enum이있는 몇 가지 케이스 ( .text및 .sticker)가 있습니다. 그리고 지금, 그렇습니다! array: [Attachments]모든 오버레이를 포함하는를 정의 할 수 있습니다 .

enum Attachment {
case text(info: Text)
case sticker(info: Stricker)
// common property between text and sticker.
var position: CGPoint {
switch self {
case .text(let info):
return info.position
case .sticker(let info):
return info.position
}
}
}
let aText = Text(content: "Hello World", font: UIFont.systemFont(ofSize: 18), position: .zero)
let aSticker = Stricker(fileURL: URL(string: "http://www.google.com/logo.png")!, position: .init(x: 100, y: 100), scale: 2)
let attachments: [Attachment] = [.text(info: aText), .sticker(info: aSticker)]
// print positions.
for attachment in attachments {
print(attachment.position)
}
view rawattachments.swift hosted with ❤ by GitHub

enum이전 섹션에서 설명한대로 어태치먼트의 위치를 ​​얻기 위해의 유형을 우회 할 수있는 방법에 주목 하십시오.

OptionSet 및 EnumSet

Objective-C는 열거 형도 가지고있었습니다. 그러나 그들은 Swift가 제공하는 강력한 도구가 아니라 C 열거 형에 더 가깝습니다. 항상에 매핑되었습니다 Integers. 그들은 종종 특별한 구현하기 위해 사용 된 데이터 유형 이라고합니다 OptionSet. 모든 사람이 평생 이러한 유형을 한 번 이상 사용해 왔습니다. 일부 API의 작동 방식을 구성하는 가능한 옵션을 지정하는 데 사용됩니다. 매우 일반적인 예는 UIViewAnimationOptions 옵션이 설정된 UIView.animate API 입니다 .

표준 OptionSet구현으로 나를 괴롭히는 것이 있습니다 . Apple 공식 문서에서 살펴 보겠습니다.

struct ShippingOptions: OptionSet {
let rawValue: Int
static let nextDay = ShippingOptions(rawValue: 1 << 0)
static let secondDay = ShippingOptions(rawValue: 1 << 1)
static let priority = ShippingOptions(rawValue: 1 << 2)
static let standard = ShippingOptions(rawValue: 1 << 3)
static let express: ShippingOptions = [.nextDay, .secondDay]
static let all: ShippingOptions = [.express, .priority, .standard]
}
view rawoptionSet1.swift hosted with ❤ by GitHub

기술적으로는 문제가 없습니다. 그러나 안전하지 않습니다. 이것을 초기화하려면 OptionSet정수를 전달해야합니다. 비트 표기법은 4 가지 가능한 값이 다음과 같다는 것을 알려줍니다.

  • 1 << 0 = (왼쪽으로 0 위치만큼 1 비트 이동) = 1
  • 1 << 1= (왼쪽으로 1 위치 씩 1 비트 이동) = 10=2
  • 1 << 2= (왼쪽으로 2 자리 씩 1 비트 이동) = 100=4
  • 1 << 3= (1 비트 씩 왼쪽으로 3 위치 이동) = 1000=8

let express1 = ShippingOperation(rawValue: 3)
express1 == ShippingOperation.express // returns true

let unknownShipping = ShippingOperation(rawValue: 16)
// ??

OptionSet에 열거 형 사용

은 무엇입니까 OptionSet? 이름과 함께 사용됩니다. 옵션 세트입니다. 그러므로 우리는 반복 된 요소를 가질 수 없습니다. 이것은 결국 집합 입니다. 그리고 집합 대수 는 작동해야합니다. 그래서… enum를 사용하여 대안을 정의하고 a Set를 사용하여 고유성과 집합 대수를 얻는 것은 어떻습니까?

enum ShippingOperation {
case nextDay
case secondDay
case priority
case standard
}
typealias ShippingOperationSet = Set<ShippingOperation>
extension ShippingOperationSet {
static var express: ShippingOperationSet { [.nextDay, .secondDay]}
static var all: ShippingOperationSet { return ShippingOperationSet.express.union([.priority, .standard]) }
}
view rawoptionSet2.swift hosted with ❤ by GitHub

Et voila! 우리는를 OptionSet이 두 가지 작은 유형으로 교체했습니다 . 우리는 OptionSet더 이상 기존 값을 만들 수없고 더 이상 실제 세계에서는 의미가없는 연산을 "합산"할 수 없습니다.

참고 : 원하는 경우 연산자를 활용하여 더 좋은 구문을 얻을 수 있습니다. 예를 들어, +이러한 유형을 사용하는 방법을 단순화하기 위해 연산자를 도입 할 수 있습니다.

extension ShippingOperationSet {
static func +(lhs: Self, rhs: Self) -> Self {
return lhs.union(rhs)
}
static func +(lhs: ShippingOperation, rhs: Self) -> Self {
return rhs.union([lhs])
}
static func +(lhs: Self, rhs: ShippingOperation) -> Self {
return lhs.union([rhs])
}
}
let setPlusOption = ShippingOperationSet.express + ShippingOperation.standard
let optionPlusSet = ShippingOperation.standard + ShippingOperationSet.express
let setPlusSet = ShippingOperationSet.express + .all
view rawoperators.swift hosted with ❤ by GitHub

가독성 : 중첩 스위치가없는 깨끗한 코드

enum일관되게 사용하는 동안 enum관련 값이 enum. 이 같은:

enum Dog {
case siberianHusky
case germanSheperd
}
enum Cat {
case siamese
case abyssian
}
enum Pet {
case cat(race: Cat)
case dog(race: Dog)
}
view rawpet.swift hosted with ❤ by GitHub

이 모델은 정확합니다. 그러나 애완 동물이 시베리안 허스키 일 때 특정 작업을 수행하려면 다음과 같은 코드를 작성해야합니다.

let myPet: Pet = .dog(race: .siberianHusky)
switch myPet {
case .dog(let dog):
switch dog {
case .siberianHusky:
print("I have a Husky!")
case .germanSheperd:
print("I have a German Sheperd!")
}
case .cat(let cat):
switch cat {
case .siamese:
print("I have a Siamese Cat!")
case .abyssian:
print("I have an Abyssian Cat !")
}
}
view rawpetUsage.swift hosted with ❤ by GitHub

이 예제는 간단하고 읽기 어렵지 않지만 실제 코드에서는 곧 매우 지저분해질 수 있습니다. 이 문제를 해결하기위한 두 가지 대안이 있습니다.

  • 메인 스위치에서 내부 케이스를 명시 적으로 풀기
  • 속성에서 내부 스위치를 숨 깁니다.

let myPet: Pet = .dog(race: .siberianHusky)
switch myPet {
case .dog(.siberianHusky):
print("I have a Husky!")
case .dog(.germanSheperd):
print("I have a German Sheperd!")
case .cat(.siamese):
print("I have a Siamese Cat!")
case .cat(.abyssian):
print("I have an Abyssian Cat !")
}

숨김 기술은 내부 스위치의 논리가 복잡 할 때 더 적합합니다. inner의 모든 경우를 처리 할 수있는 속성 또는 함수를 만들 수 있습니다 switch. 그런 다음 main switch. 이 접근 방식의 이점은 다음과 같습니다.

  • 코드의 한 지점에서 다른 지점으로 이동해야 함에도 불구하고 읽기 쉬운 코드
  • 두 기능 / 속성을 개별적으로 테스트 할 수있는 가능성.
  • let myPet: Pet = .dog(race: .siberianHusky)
    switch myPet {
    case .dog(let dog):
    handle(dog: dog)
    case .cat(let cat):
    handle(cat: cat)
    }
    // Helpers
    func handle(dog: Dog) {
    switch dog {
    case .siberianHusky:
    print("I have a Husky!")
    case .germanSheperd:
    print("I have a German Sheperd!")
    }
    }
    func handle(cat: Cat) {
    switch cat {
    case .siamese:
    print("I have a Siamese Cat!")
    case .abyssian:
    print("I have an Abyssian Cat !")
    }
    }

오늘 기사에서는 열거 형을 살펴보고 앱에서 더 효율적이고 효과적인 방식으로 열거 형을 사용하는 방법을 살펴 보았습니다. 우리는보다 표현적이고 가독성이 높으며 안전한 코드를 작성하는 방법을 알아 냈습니다.

나는 이것이 모두가 더 깨끗하고 읽기 쉬운 코드를 작성하고 소프트웨어 엔지니어링 기술을 향상시키는 데 도움이되기를 바랍니다!

댓글 없음:

댓글 쓰기