반응형

원문 : https://www.raywenderlich.com/139591/building-custom-collection-swift

배열(arrays), 딕셔너리(dictionaries), 세트(sets) 모두 일반적으로 사용되는 컬렉션 타입의 예 입니다.

하지만 앱에 바로 사용하는 필요한 모든 것을 제공하지 않으면 어떻게 될까요?

일반적인 해결책은 데이터를 정리하는 비즈니스 로직과 함께 Array나 Dictionary를 사용하는 것입니다. 이것은 직관적이지 않고 유지하기 어려워서 문제가 될수 있습니다.

이것이 자신만의 컬렉션 타입을 만드는 것과 관련있습니다. 여기에서, Swift의 강력한 컬렉션 프로토콜을 사용해서 사용자지정(custom) 컬렉션을 만들것입니다.

주의
이 튜토리얼은 Swift 3.0으로 작업되었습니다. 이전 버젼은 Swift 표준 라이브러리의 메이져(major)가 변경되서 컴파일 되지 않습니다.

시작하기(Getting Started)

이 튜토리얼에서, 스크래치(scratch)로부터 멀티세트(multiset or bag)를 만들것 입니다.

bag은 반복되는 값 없이 객체를 저장한다는 점에서 set과 비슷합니다. set에서, 중복된 객체는 무시됩니다. 반면에, bag은 각 객체에 대한 실행 횟수를 유지합니다.

이것에 대한 훌륭한 예제가 쇼핑 목록(shopping list) 입니다. 항목별로 각각에 대한 관련 수량을 가진 식료품 목록을 원할 것입니다. 중복된 항목 인스턴스를 추가하는 대신에, 기존 항목의 수량을 증가 시킬것이라 예상합니다.

컬렉션 프로토콜로 넘어가기 전에, 먼저 Bag의 기본 구현을 작성합니다.

첫번째, 새로운 플레이그라운드를 만듭니다: Xcode에서 File\New\Playground..을 선택하고 플레이그라운드 이름을 Bag으로 합니다. 이 튜토리얼은 플랫폼에 구애받지 않고 Swift 언어에만 초점을 두기 때문에, 어떤 플랫폼도 선택 할수 있습니다. Next 클릭하고, 플레이그라운드를 저장하기 편한 위치를 선택하고 Create를 클릭합니다.

다음으로, 플레이그라운드의 내용을 bag의 비어있는 구현으로 교체합니다.

struct Bag<Element: Hashable> {
 
}

Bag은 Hashable 요소 타입이 필요한 제네릭 구조체입니다. 요구되는 Hashable요소는 O(1) 시간복잡도(time complexity)에서 고유한 값을 비교하고 저장할 수 있습니다. 이것은 내용(contents)의 크기와 상관없이, Bag은 일정한 속도로 실행될 것입니다. Swift의 표준 컬렉션의 값으로 구조체(struct)를 사용하였습니다.

다음으로, Bag에 다음 프로퍼티들을 추가하세요.

// 1
fileprivate var contents: [Element: Int] = [:]
 
// 2
var uniqueCount: Int {
  return contents.count
}
 
// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

이것들은 bag에서 필요한 기본 프로퍼티들입니다. 다음은 각각의 하는 일입니다.

  1. 내부 데이터 구조로 Dictionary를 사용하고 있습니다. 이 작업은 요소들을 저장하는데 사용하는 고유한 키를 적용하기 때문에 bag에 유용합니다. 각 요소에 대한 딕셔너리 값은 갯수입니다. 외부에서 bag의 내부 작업을 숨기기 위해 fileprivae로 표시하였습니다.
  2. uniqueCount는 개별 수량을 무시하고, 고유 항목들의 갯수를 반환합니다. 예를들어, 4개의 오렌지와 2개의 사과 가 있는bag에서 uniqueCount는 2를 반환합니다.
  3. totalCount는 bag에 있는 항목들의 전체 갯수를 반환합니다. 이전과 같은 예제에서, totalCount는 6을 반환합니다.

이제 Bag의 내용(contents)를 편집하기 위한 메소드 몇개가 필요할 것입니다. 방금전에 추가한 프로퍼티 아래에 다음 메소드를 추가하세요.

// 1
mutating func add(_ member: Element, occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0, "Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

여기서 하는 일은 다음과 같습니다.

  1. add(_:occurrences:)은 bag에 요소를 추가하는 방법을 제공합니다. 그것은 두개의 매개변수를 가집니다. : 제네릭 타입의 Element와 옵셔널인 발생 횟수입니다. mutating 키워드는 struct나 enum에서 변수를 수정하는데 사용됩니다. 이 메소드는 인스턴스가 var이 아닌 상수 let으로 정의되었으면 사용할수 없스니다.
  2. precondition(_:_:)은 첫번째 매개변수로 Boolean 값을 가집니다. 만약 false라면, 프로그램의 실행은 멈출것이고 두번째 매개변수의 String을 디버그 영역에 출력할 것입니다. 이 튜토리얼에서 Bag이 의도된대로 사용되었는지 보증하기 위해서 전제조건(precondition)을 여러번 사용할 것입니다. 또한 기능을 추가할때 예상대로 동작하는지 판단하기 위해 사용할 것입니다.
  3. 요소(element)가 이미 bag에 존재하는지 확인합니다. 만약 그렇다면 갯수를 증가시키고, 그렇지 않으면 새로운 요소를 생성합니다.

다른 측면에서, Bag에서 요소를 삭제하는 방법이 필요할것입니다. add(_:occurences:)아래에 다음 메소드를 추가하세요.

mutating func remove(_ member: Element, occurrences: Int = 1) {
  // 1
  guard let currentCount = contents[member], currentCount >= occurrences else {
    preconditionFailure("Removed non-existent elements")
  }

  // 2
  precondition(occurrences > 0, "Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

remove(_:occurrences:)는 add(_:occurrences:)와 같은 매개변수를 가지고 정반대(opposite)의 동작을 합니다. 다음은 동작 방식입니다.

  1. 첫번째로 요소가 존재하는지와 최소한으로 삭제할 갯수를 확인합니다.
  2. 다음으로 삭제할 갯수가 0보다 큰지 확인 합니다.
  3. 마지막으로, 요소가 존재하는지 확인하고 갯수를 감소시킵니다. 만약 갯수가 0으로 떨어지면, 요소는 완전히 제거된것입니다.

이 시점에 Bag은 하는게 없습니다. 내용(contents)은 한번 추가되면 접근할 수 없습니다. Dictionary에서 사용할수 있는 유용한 고차원(high-order)의 메소드를 잃게 됩니다.

하지만 모든것을 잃는것은 아닙니다. 모든 래퍼(wrapper) 코드를 자신의 객체로 분리하는 과정을 시작했습니다. 그것은 코드를 명확하게 유지하는 옳은 방향에서의 첫 걸음입니다.

하지만 기다리세요, 더 많은 것이 있습니다. Swift는 Bag을 합법적인 컬렉션으로 만드는데 필요한 모든 도구를 제공합니다.

Swift에서 컬랙션 객체를 만드는 것이 무엇인지 알아야 합니다.

사용자 지정 컬렉션은 무엇인가요?(What’s a Custom Collection?)

Swift Collection이 무엇인지 이해하려면, 먼저 프로토콜 상속 계층구조를 살펴볼 필요가 있습니다.

Sequence프로토콜은 순차적으로 제공하는 타입을 표현하며, 요소들에 반복적으로 접근합니다. 시퀀스(sequence)를 한번에 하나씩 요소 위로 이동할수 있는 항목들의 목록이라고 생각할 수 있습니다.

반복(iteration)은 간단한 개념이지만, 객체에 거대한 기능을 제공합니다. 그것은 다음과 같이 다양한 강력한 작업을 수행할 수 있습니다.

  • map(_:) : 제공된 클로져를 사용해서 시퀀스(sequence)에 있는 각 요소를 변형한 결과의 배열을 반환합니다.
  • filter(_:) : 제공된 클로져를 사용해서 충족하는(satisfy) 요소의 배열을 반환합니다.
  • reduce(_:_) : 제공된 클로져를 사용해서 시퀀스(sequence)에 있는 각 요소들을 결합한 하나의 값을 반환합니다.
  • sorted(by:) : 제공된 클로져를 사용해서 시퀀스(sequence)에 따라 정렬된 요소들의 배열을 반환합니다.

이것은 겨우 수박 겉핥기일 뿐입니다(This barely scratches the surface). Sequence에서 사용가능한 모든 메소드들을 보려면, Sequence docs를 보세요.

Sequence의 한가지 주의사항은 그것이 파괴(destructive)되거나 아니던간에 준수하는 타입에 대한 요구사항이 없습니다. 이것은 반복된 후에, 미래에 반복이 처음부터 시작할 것이라는 것을 보증하지 않는다는 의미입니다.

데이터를 한번 이상 반복할 계획이라면, 그것은 매우 큰 문제입니다. 파괴하지 않고 반복을 수행하려면, 객체는 Collection 프로토콜을 준수해야 합니다.

Collection은 Sequence와 Indexable로 부터 상속받습니다. 주요 차이점은 컬렉션(collection)은 여러번 횡단할수(traverse) 있고 인덱스별로 접근할 수 있는 하나의 시퀀스(sequence) 입니다.

Collection을 준수해서 많은 메소드와 프로퍼티를 무료로 얻을 수 있습니다. 몇가지 예입니다.:

  • isEmpty : 컬렉션이 비었거나 아니거나를 가리키는 boolean을 반환합니다.
  • first : 컬렉션에 있는 첫번째 요소를 반환합니다.
  • count : 컬렉션에 있는 요소들의 갯수를 반환합니다.

컬렉션(collection)에 있는 요소들의 타입에 따라 더 많은 것들이 사용가능합니다. 만약 살짝 들여다 보고 싶다면 Collection docs를 확인하세요.

이러한 프로토콜을 구현하기 전에, Bag을 쉽게 개선할 수 있습니다.

텍스트 표현(Textual Representation)

현재의 Bag객체는 print(_:)나 결과 사이드바(results sidebar)를 통해서 약간의 정보가 드러납니다.

플레이그라운드의 끝에 다음 코드를 추가하세요.

var shoppingCart = Bag()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

이렇게 과일 몇개가 들어가 있는 새로운 Bag객체를 만듭니다. 플레이그라운드 디버거를 보면, 어떠한 내용도 없는 객체 타입을 볼것입니다.

Swift 표준 라이브러리에 의해 제공되는 단일 프로토콜을 사용해서 해결 할수 있습니다.

shoppingCard위의 Bag의 닫힘 꺽쇠(}) 이후에 다음을 추가하세요.

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

CustomStringConvertible요구사항을 준수하기 위해 description 이름을 가진 단일 프로퍼티를 구현합니다. 이 프로퍼티는 지정된 인스턴스의 텍스트 표현을 반환합니다.

이것은 데이터를 표현하는 문자열을 만드는데 필요한 로직을 넣는 곳입니다. Dictionary는 CustomStringConvertible를 준수하기 때문에, 단순하게 contents로부터 description의 값을 재사용합니다.

shoppingCart에 대해 이전에 무의미했던 디버그 정보를 보세요.

대단합니다! 이제 Bag에 기능을 추가하면, 내용을 확인 할수 있을 것입니다.

플레이그라운드에서 코드를 작성하는 동안, precondition(_:_:) 호출로 예상되는 결과를 확인할 수 있습니다. 점진적으로 이전에 작업했던 기능을 실수로 멈추지 않도록 할것입니다. 이 툴을 단위테스트에서도 마찬가지로 사용할 수 있습니다. - 이것을 매일 코딩하여 통합하는게 좋습니다!

플레이그라운드의 끝에 remove(_:occurrences:)을 마지막으로 호출한 후에 다음을 추가하세요.

precondition("\(shoppingCart)" == "\(shoppingCart.contents)", "Expected bag description to match its contents description")

contents로 부터 shoppingCart의 설명이 다르면 오류가 발생할 것입니다.

강력한 컬렉션 타입을 만들기 위한 다음 단계는 초기화 입니다.

초기화(Initialization)

한번에 하나의 요소를 추가하는 것은 꽤 성가십니다(annoying). 공통적으로 기대되는 한가지는 다른 컬렉션으로 콜렉션 타입을 초기화하는게 가능하다는 것입니다.

Bag을 만드는 방법은 다음과 같습니다. 플레이그라운드의 끝에 다음 코드를 추가하세요.

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary, "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary) 
precondition(dictionaryBag.contents == dataDictionary, "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1], "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

이 타입에 대한 어떤 초기화도 정의하지 않았기 때문에 컴파일 되지 않습니다. 각 타입에 대한 초기화 메소드로 명시적으로 생성하는 대신에, 제네릭을 사용할 것입니다.

다음 메소드를 Bag의 구현 안쪽에 totalCount 바로 아래에 추가하세요.

// 1
init() { }

// 2
init(_ sequence: S) where S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init(_ sequence: S) where S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

방금 추가한 내용을 살펴 봅시다.

  1. 먼저, 빈 초기화 메소드를 만들었습니다. 컴파일러 오류를 피하기 위해 추가적으로 init메소드를 정의한 후에 추가해야 합니다.
  2. 다음으로, Sequence의 요소를 받아들이는 초기화 메소드를 추가하였습니다. 이 시퀀스(sequence)는 반드시 Element타입과 일치해야 합니다. 예를들어, Array와 Set객체 모두 처리합니다. 전달받은 시퀀스(sequence)대로 반복하고 각 요소를 한번에 하나씩 추가합니다.
  3. 마지막 메소드도 비슷하게 작업하지만, (Element, Int)타입의 튜플 요소에 대해 동작합니다. 이 예제에서는 Dictionary입니다. 여기에서, 시퀀스(sequence)의 각 요소를 반복하고 지정된 갯수를 추가합니다.

이러한 제네릭 초기화는 Bag 객체들에 대해 훨씬 다양한 데이터 소스를 지원합니다. 하지만, Bag에 다른 시퀀스(sequence)로 전달하면 초기화가 필요합니다.

이를 피하기 위해, Swift 표준 라이브러리는 두개의 프로토콜을 지원합니다. 이러한 프로토콜들은 시퀀스 원문(sequence literals)으로 초기화할 수있습니다. 원문(literals)은 명시적으로 객체를 만들지 않아도 축약법으로 데이터를 작성할수 있습니다.

이것을 어떻게 사용하는지 보기위해 플레이그라운드의 끝에 다음 코드를 추가하세요.

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary, "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary, "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

또 다시, 다음에 고쳐야할 컴파일러 오류를 볼것입니다. 이것은 객체보다 Array와 Dictionary 원문(literals)를 사용한 초기화 예제입니다.

다른 Bag 확장 바로 아래에 다음 두 확장을 추가하세요.

extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    // The map converts elements to the "named" tuple the initializer expects.
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}

ExpressibleByArrayLiteral과 ExpressibleByDictionaryLiteral 모두 원문(literal) 매개변수와 일치하는 것을 처리하는 초기화가 필요합니다. 이전에 추가한 초기화때문에 쉽게 구현할수 있었습니다.

Bag은 순수한(native) 컬렉션 타입과 더 비슷하게 보이며, 이제는 마법을 부릴때입니다.

시퀀스(Sequence)

컬렉션 타입에서 실행된 가장 일반적인 동작은 바로 요소들을 반복하는 것입니다. 이 예제를 보려면, 플레이그라운드의 끝에 다음을 추가하세요.

for element in shoppingCart {
  print(element)
}

가장 기본이 되는 것은 여기에 있습니다. Array와 Dictionary와 마찬가지로, bag으로 반복문이 가능해야 합니다. 현재의 Bag타입은 Sequence를 준수하지 않기 때문에 컴파일 되지 않습니다.

ExpressibleByDictionaryLiteral확장 바로 뒤에 다음을 추가하세요.

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

이것들은 Sequence를 준수하는데 별로 필요하지 않습니다. 방금 추가한것을 살펴봅시다.

  1. Sequence가 IteratorProtocol를 준수하는 것으로 정의하는 Iterator를 typealias로 정의하였습니다. DictionaryIterator은 Dictionary객체가 요소들을 반복하는데 사용하는 타입입니다. Bag의 기본(underlying) 데이터를 Dictionary에 저장하기 때문에 이 타입을 사용합니다.
  2. makeIterator()은 시퀀스(sequence)의 각 요소를 단계별로 처리 할수 있는 Iterator을 반환합니다.
  3. contents에서 makeIterator()호출로 Sequence를 이미 준수하는 반복자(iterator)를 만듭니다.

Sequence를 준수하는 Bag을 만들때 필요한 전부입니다.

이제 Bag의 각 요소를 통해 반복할수 있고 각 객체에 대한 갯수를 가져올 수 있습니다. 플레이그라운드 끝에 이전의 for-in반복문 뒤에 다음을 추가하세요.

for (element, count) in shoppingCart {
  print("Element: \(element), Count: \(count)")
}

디버그 영역을 열고 시퀀스(sequence)에 있는 요소들이 출력되는 것을 볼수 있을것입니다.

Bag을 통한 반복이 가능한 것은 Sequence에 의해 구현된 유용한 많은 메소드들을 사용할수 있습니다.

이것들의 일부가 실제 동작하는지 보기 위해 플레이그라운드의 끝에 다음을 추가하세요.

// Find all elements with a count greater than 1
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected moreThanOne contents to be [(\"Banana\", 2)]")

// Get an array of all elements without counts
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(itemList == ["Orange", "Banana"], "Expected itemList contents to be [\"Orange\", \"Banana\"]")

// Get the total number of items in the bag
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3, "Expected numberOfItems contents to be 3")

// Get a sorted array of elements by their count in decending order
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(sorted.first!.key == "Banana" && moreThanOne.first!.value == 2, "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

이것들 모두 시퀀스(sequence)로 작업하기 위한 유용한 메소드들입니다. - 그리고 그것들은 실제적으로 무료입니다.

이제, Bag을 사용해서 내용(content)을 만들수 있지만, 그게 재미있을까요? 현재 Sequence 구현을 확실이 향상시킬수 있습니다.

시퀀스 향상시키기(Improving Sequence)

현재, 무거운 짐을 처리하기 위해 Dictionary에 의존하고 있습니다. 그것은 자신만의 강력한 컬렉션을 쉽게 만들수 있기 때문에 괜찮고 멋집니다. 문제는 Bag사용자에게 이상하고 혼란스러운 상황을 만듭니다.

예를들어, Bag이 DictionaryIterator타입의 반복자(iterator)를 반환하는 것이 직관적이지 않습니다. 자신만의 반복자(iterator) 타입을 만드는 것은 확실히 가능하지만, 다행스럽게도 필요하지는 않습니다.

Swift는 바깥쪽에 기본 반복자(iterator)를 숨기기 위해서 AnyInterator 타입을 제공합니다.

Sequence 확장의 구현을 다음으로 교체 합니다.

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator<(element: Element, count: Int)>

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

플레이그라운드에서 해결해야할 몇가지 오류를 볼수 있을것입니다. AnyIterator 추가된 이전의 구현과 유사합니다.

  1. AnyIterator는 기본 반복자(iterator)에 next()메소드를 전달하는 타입이 제거된 반복자(iterator)입니다. 이것으로 실제 사용된 반복자(iterator) 타입을 숨길수 있습니다.
  2. 이전처럼, contents로 새로운 DictionaryIterator를 생성합니다.
  3. 마지막으로, next()메소드를 전달하기 위해서 새로운 AnyIterator객체에서 iterator를 래핑(wrap)합니다.

이제 오류를 수정하세요. 다음 두가지 오류를 보게 될 것입니다.

이전에는, DictionaryIterator 튜플의 key와 value 이름을 사용하였습니다. 바깥쪽으로 부터 DictionaryIterator을 숨기고 튜플의 이름을 element와 count로 변경하였습니다. 오류를 수정하기 위해, key와 value를 element와 count로 각각 교체하세요.

전제조건(precondition)은 이제 전과 마찬가지로 통과하고 동작해야 합니다. 전제조건(precondition)은 예기치 않게 바뀌지 않도록 만들어 주기위해 사용합니다.

이제 아무도 딕셔너리(dictionary)를 사용하는 것을 알수 없습니다.

이제는 Bag에 대한 기분이 더 좋아졌으니, 집에 갈때입니다. 좋습니다. 좋아요. 이제 여러분의 흥분을 모아서(collect), Collection을 할 때입니다.

Collection

더 이상 고민하지 말고, 여기에 컬렉션을 만드는 진정한 방법은 Collection프로토콜입니다. 다시 말하면, Collection은 파괴하지 않고(nondestructively) 인덱스로 사용하고 여러번 횡단할수(traverse)있는 시퀀스(sequence)입니다.

Collection준수를 추가하려면, 다음에 오는 세부정보를 제공해야 합니다.

  • startIndex와 endIndex : 컬렉션의 범위를 정의하고 횡단하는(transversal) 시작점을 표시합니다.
  • subscript(positon:) : 인덱스를 사용해서 컬렉션내의 모든 요소를 접근할 수 있습니다. 이러한 접근은 O(1) 시간 복잡성으로 실행되야 합니다.
  • index(after:) : 인덱스로 전달된 바로 뒤의 인덱스를 반환합니다.

컬렉션으로 작업하는 4가지 세부 사항만 있습니다. Sequence확장 바로 뒤에 다음 코드를 추가하세요.

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position), "out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key, count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

이것은 매우 간단합니다. :

  1. 첫번째, Collection에 DictionaryIndex로 정의된 Index타입을 선언합니다. 이러한 인덱스를 contents에 전달할 것입니다. 컴파일러가 나머지 구현을 기반으로 이 타입을 추론할수 있다는 것을 주의하세요. 코드를 명확하고 유지보수하기 쉽게 유지하도록 명시적으로 선언합니다.
  2. 다음으로, contents의 시작과 마지막 인덱스를 반환합니다.
  3. 여기에서, 전제조건(precondition)을 사용해서 유효한 인덱스 수행합니다. 인덱스에 해당하는 contents의 값을 새로운 튜플을 반환합니다.
  4. 마지막으로, contents에서 호출된 index(afert:)의 값을 되돌려 줍니다.

이러한 프로퍼티와 메소드를 추가하면, 모든 기능을 가진 컬렉션을 만들었습니다. 새로운 기능중 일부를 테스트 하기 위해 플레이그라운드의 끝에 다음 코드를 추가하세요.

// Get the first item in the bag
let firstItem = shoppingCart.first
precondition(firstItem!.element == "Orange" && firstItem!.count == 1, "Expected first item of shopping cart to be (\"Orange\", 1)")

// Check if the bag is empty
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false, "Expected shopping cart to not be empty")

// Get the number of unique items in the bag
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2, "Expected shoppingCart to have 2 unique items")

// Find the first item with an element of "Banana"
let bananaIndex = shoppingCart.indices.first { shoppingCart[$0].element == "Banana" }!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2, "Expected banana to have value (\"Banana\", 2)")

굉장합니다!

(여러분이 했던 일에 대해 좋은 기분을 느낄수 있지만, 잠깐만요, 더 잘할 수 있어라는 말을 합니다.)

글쎄, 여러분이 옳습니다. 더 잘할수 있습니다. 여전히 Bag에서 약간 Dictionary 냄새가 납니다.

컬렉션 개선하기(Improving Collection)

Bag이 너무 많은 내부동작을 보여줍니다. Bag의 사용자는 컬렉션에 있는 요소들에 접근하기 위해 DictionaryIndex 객체를 사용해야 합니다.

여러분은 이것을 쉽게 해결 할수 있습니다. Collection 확장 바로 뒤에 다음을 추가하세요.

// 1
struct BagIndex {
  // 2
  fileprivate let index: DictionaryIndex

  // 3
  fileprivate init(_ dictionaryIndex: DictionaryIndex) {
    self.index = dictionaryIndex
  }
}

여기에 너무 이상한 것은 없지만, 방금 막 추가한것이 무엇을 하는지 살펴 봅시다.

  1. 새로운 제네릭(generic) 타입 BagIndex를 정의하였습니다. Bag과 마찬가지로, 딕셔너리(dictionaries)에서 사용할수 있도록 Hashable인 제네릭 타입이 필요합니다.
  2. 인덱스 타입의 기본 데이터는 fileprivate DictionaryIndex 객체입니다. BagIndex는 실제로 바깥쪽에 진정한 인덱스(true index)를 숨기는 래퍼(wrapper)일 뿐입니다.
  3. 마지막으로, 저장할 DictionaryIndex를 허용하는 fileprivate 초기화를 생성합니다.

Collection은 두개의 인덱스를 비교하는 연산을 실행하기 위해 비교 가능한 Index가 필요합니다. 이 때문에, BagIndex는 Comparable을 준수하는게 필요합니다. BagIndex바로 뒤에 다음에 오는 확장(extension)을 추가하세요.

extension BagIndex: Comparable {
  static func == (lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func < (lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

그 로직(logic)은 간단합니다. 정확한 값을 반환하기 위해 DictionaryIndex의 Comparable 메소드를 사용합니다.

이제 BagIndex를 사용하도록 Bag을 업데이트 할 준비가 되었습니다. Collection확장을 다음으로 교체하세요.

extension Bag: Collection {
  // 1
  typealias Index = BagIndex

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position), "out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key, count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

번호가 있는 각 주석은 변경된것을 표시합니다. 무엇인지는 다음과 같습니다.

  1. 먼저 DictionaryIndex를 BagIndex로 Index타입을 교체하였습니다.
  2. 다음으로, startIndex와 endIndex 모두, contents로 부터 새로운 BagIndex를 만들었습니다.
  3. 그리고나서 contents의 요소(element)에 접근하고 반환하기 위해BagIndex의 프로퍼티를 사용하였습니다.
  4. 마지막으로, 이전 단계의 조합을 사용했습니다. BagIndex의 프로퍼티를 사용해서 contents에서 DictionaryIndex값을 가져옵니다. 그런다음 이 값을 사용해서 새로운 BagIndex를 생성했습니다.

바로 그것입니다! 사용자는 데이터를 어떻게 저장하는지 아무것도 알지 못합니다. 인덱스 객체들을 훨씬 효과적으로 제어할 수 있습니다.

이것을 마무리하기 전에, 더 중요한 주제가 하나 있습니다. 인덱스 기반의 접근을 추가해서, 컬렉션 안의 값의 범위(range)를 인덱스 할수 있습니다.

마무리를 위해, Slice을 컬렉션(collections)과 함께 동작하는 방법을 살펴 볼것입니다.

자르기(Slice)

Slice는 컬렉션 내의 요소의 하위시퀀스(subsequence)로 보여집니다. 복사본을 만들지 않고 특정 요소의 하위시퀀스에 대한 동작을 실행할 수 있습니다.

하나의 슬라이스(slice)는 생성된 기본 컬렉션에 대한 참조를 저장합니다. 그것은 또한, 서브시퀀스 범위를 표시하기 위해서 시작과 끝 인덱스에 대한 참조를 유지합니다. 슬라이스(Slices)는 기본 컬렉션을 직접 참조하기 때문에 O(1) 복잡성을 유지 합니다.

슬라이스(Slices)는 인덱스를 기본 컬렉션과 공유하므로 믿을수없을 만큼 유용합니다.

실제로 어떻게 동작하는지 보기 위해, 플레이그라운드의 끝에 다음에 오는 코드를 추가하세요.

// 1
let fruitBasket = Bag(dictionaryLiteral: ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst() // No pun intended ;]

// 3
if let fruitMinIndex = fruitSlice.indices.min(by: { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let minFruitFromSlice = fruitSlice[fruitMinIndex]
  let minFruitFromBasket = fruitBasket[fruitMinIndex]
}

여기에서 무엇을 하는지 그리고 그것이 중요한 이유가 무엇인지 살펴봅시다.

  1. 먼저 4개의 다른 과일로 구성된 과일 바구니를 생성합니다.
  2. 다음으로, 먹기 위해 과일의 첫번째 타입을 제거합니다. 제거된 첫번째 요소를 제외한 과일바구니로 새로 생성된 Slice를 봅니다.
  3. 슬라이스(slice)에 남아있는 과일로, 가장 적은 과일의 인덱스를 찾습니다.
  4. 슬라이스(slice)에서 계산된 인덱스를 이용해서, 슬라이스(slice) 뿐만아니라 기본 컬렉션에서도 성공적으로 사용 할수 있습니다.

주의
슬라이스(Slice)가 의미있는 방법으로 순서가 정의되지 않았기 때문에, Dictionary과 Bag같은 해시 기반의 컬렉션에 비해 조금 덜 유용해 보일수 있습니다. 반면에 Array은 하위시퀀스 연산을 수행할때 슬라이스(slice)가 큰 역할을 하는 컬렉션 타입의 훌륭한 예입니다.

축하합니다. 여러분은 이제 컬렉션(collection) 전문가입니다! 원하는 만큼의 bag을 만들어 축하할수 있습니다.

여기에서 어디로 가야하나요?(Where to Go From Here?)

여기서 사용된 모든 코드로 완성된 플레이그라운드를 다운로드 할수 있습니다. 다운로드 
좀 더 완벽한 Bag구현을 보거나 기여(contribute)하고 싶으면, github를 확인하세요.

여기에서 Swift에서 데이터 구조를 자신이 원하는데로 컬렉션으로 만드는 방법에 대해서 배웠습니다. SequenceCollectionCustomStringConvertibleExpressibleByArrayLiteralExpressibleByDictionaryLiteral을 준수하도록 추가해서 자신만의 인덱스 타입을 만들수 있습니다.

이것은 단지 Swift가 제공하는 강력하고 유용한 컬렉션을 제공하는 모든 프로토콜들 중에 맛보기일 뿐입니다. 여기에서 다루지 않은 것에 대해서 읽고 싶으면, 다음에 오는 것들을 확인하세요.

이 튜토리얼을 즐겼기를 바랍니다. 자신의 컬렉션을 만드는 것이 가장 흔한 요구사항이 아닐수 있지만, Swift가 제공하는 컬렉션 타입을 더 잘 이해할수 있길 바랍니다.

반응형
Posted by 까칠코더
,