원문 : 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
에서 필요한 기본 프로퍼티들입니다. 다음은 각각의 하는 일입니다.
- 내부 데이터 구조로
Dictionary
를 사용하고 있습니다. 이 작업은 요소들을 저장하는데 사용하는 고유한 키를 적용하기 때문에bag
에 유용합니다. 각 요소에 대한 딕셔너리 값은 갯수입니다. 외부에서bag
의 내부 작업을 숨기기 위해fileprivae
로 표시하였습니다. uniqueCount
는 개별 수량을 무시하고, 고유 항목들의 갯수를 반환합니다. 예를들어, 4개의 오렌지와 2개의 사과 가 있는bag
에서uniqueCount
는 2를 반환합니다.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
}
}
여기서 하는 일은 다음과 같습니다.
add(_:occurrences:)
은bag
에 요소를 추가하는 방법을 제공합니다. 그것은 두개의 매개변수를 가집니다. : 제네릭 타입의Element
와 옵셔널인 발생 횟수입니다.mutating
키워드는struct
나enum
에서 변수를 수정하는데 사용됩니다. 이 메소드는 인스턴스가var
이 아닌 상수let
으로 정의되었으면 사용할수 없스니다.precondition(_:_:)
은 첫번째 매개변수로 Boolean 값을 가집니다. 만약false
라면, 프로그램의 실행은 멈출것이고 두번째 매개변수의String
을 디버그 영역에 출력할 것입니다. 이 튜토리얼에서Bag
이 의도된대로 사용되었는지 보증하기 위해서 전제조건(precondition)을 여러번 사용할 것입니다. 또한 기능을 추가할때 예상대로 동작하는지 판단하기 위해 사용할 것입니다.- 요소(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)의 동작을 합니다. 다음은 동작 방식입니다.
- 첫번째로 요소가 존재하는지와 최소한으로 삭제할 갯수를 확인합니다.
- 다음으로 삭제할 갯수가 0보다 큰지 확인 합니다.
- 마지막으로, 요소가 존재하는지 확인하고 갯수를 감소시킵니다. 만약 갯수가 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)
}
}
방금 추가한 내용을 살펴 봅시다.
- 먼저, 빈 초기화 메소드를 만들었습니다. 컴파일러 오류를 피하기 위해 추가적으로
init
메소드를 정의한 후에 추가해야 합니다. - 다음으로,
Sequence
의 요소를 받아들이는 초기화 메소드를 추가하였습니다. 이 시퀀스(sequence)는 반드시Element
타입과 일치해야 합니다. 예를들어,Array
와Set
객체 모두 처리합니다. 전달받은 시퀀스(sequence)대로 반복하고 각 요소를 한번에 하나씩 추가합니다. - 마지막 메소드도 비슷하게 작업하지만,
(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
를 준수하는데 별로 필요하지 않습니다. 방금 추가한것을 살펴봅시다.
Sequence
가IteratorProtocol
를 준수하는 것으로 정의하는Iterator
를typealias
로 정의하였습니다.DictionaryIterator
은Dictionary
객체가 요소들을 반복하는데 사용하는 타입입니다.Bag
의 기본(underlying) 데이터를Dictionary
에 저장하기 때문에 이 타입을 사용합니다.makeIterator()
은 시퀀스(sequence)의 각 요소를 단계별로 처리 할수 있는Iterator
을 반환합니다.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
추가된 이전의 구현과 유사합니다.
AnyIterator
는 기본 반복자(iterator)에next()
메소드를 전달하는 타입이 제거된 반복자(iterator)입니다. 이것으로 실제 사용된 반복자(iterator) 타입을 숨길수 있습니다.- 이전처럼,
contents
로 새로운DictionaryIterator
를 생성합니다. - 마지막으로,
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)
}
}
이것은 매우 간단합니다. :
- 첫번째,
Collection
에DictionaryIndex
로 정의된Index
타입을 선언합니다. 이러한 인덱스를contents
에 전달할 것입니다. 컴파일러가 나머지 구현을 기반으로 이 타입을 추론할수 있다는 것을 주의하세요. 코드를 명확하고 유지보수하기 쉽게 유지하도록 명시적으로 선언합니다. - 다음으로,
contents
의 시작과 마지막 인덱스를 반환합니다. - 여기에서, 전제조건(precondition)을 사용해서 유효한 인덱스 수행합니다. 인덱스에 해당하는
contents
의 값을 새로운 튜플을 반환합니다. - 마지막으로,
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
}
}
여기에 너무 이상한 것은 없지만, 방금 막 추가한것이 무엇을 하는지 살펴 봅시다.
- 새로운 제네릭(generic) 타입
BagIndex
를 정의하였습니다.Bag
과 마찬가지로, 딕셔너리(dictionaries)에서 사용할수 있도록Hashable
인 제네릭 타입이 필요합니다. - 인덱스 타입의 기본 데이터는
fileprivate DictionaryIndex
객체입니다.BagIndex
는 실제로 바깥쪽에 진정한 인덱스(true index)를 숨기는 래퍼(wrapper)일 뿐입니다. - 마지막으로, 저장할
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))
}
}
번호가 있는 각 주석은 변경된것을 표시합니다. 무엇인지는 다음과 같습니다.
- 먼저
DictionaryIndex
를BagIndex
로Index
타입을 교체하였습니다. - 다음으로,
startIndex
와endIndex
모두,contents
로 부터 새로운BagIndex
를 만들었습니다. - 그리고나서
contents
의 요소(element)에 접근하고 반환하기 위해BagIndex
의 프로퍼티를 사용하였습니다. - 마지막으로, 이전 단계의 조합을 사용했습니다.
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]
}
여기에서 무엇을 하는지 그리고 그것이 중요한 이유가 무엇인지 살펴봅시다.
- 먼저 4개의 다른 과일로 구성된 과일 바구니를 생성합니다.
- 다음으로, 먹기 위해 과일의 첫번째 타입을 제거합니다. 제거된 첫번째 요소를 제외한 과일바구니로 새로 생성된
Slice
를 봅니다. - 슬라이스(slice)에 남아있는 과일로, 가장 적은 과일의 인덱스를 찾습니다.
- 슬라이스(slice)에서 계산된 인덱스를 이용해서, 슬라이스(slice) 뿐만아니라 기본 컬렉션에서도 성공적으로 사용 할수 있습니다.
주의
슬라이스(Slice)가 의미있는 방법으로 순서가 정의되지 않았기 때문에,Dictionary
과Bag
같은 해시 기반의 컬렉션에 비해 조금 덜 유용해 보일수 있습니다. 반면에Array
은 하위시퀀스 연산을 수행할때 슬라이스(slice)가 큰 역할을 하는 컬렉션 타입의 훌륭한 예입니다.
축하합니다. 여러분은 이제 컬렉션(collection) 전문가입니다! 원하는 만큼의 bag
을 만들어 축하할수 있습니다.
여기에서 어디로 가야하나요?(Where to Go From Here?)
여기서 사용된 모든 코드로 완성된 플레이그라운드를 다운로드 할수 있습니다. 다운로드
좀 더 완벽한 Bag
구현을 보거나 기여(contribute)하고 싶으면, github를 확인하세요.
여기에서 Swift에서 데이터 구조를 자신이 원하는데로 컬렉션으로 만드는 방법에 대해서 배웠습니다. Sequence, Collection, CustomStringConvertible, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral을 준수하도록 추가해서 자신만의 인덱스 타입을 만들수 있습니다.
이것은 단지 Swift가 제공하는 강력하고 유용한 컬렉션을 제공하는 모든 프로토콜들 중에 맛보기일 뿐입니다. 여기에서 다루지 않은 것에 대해서 읽고 싶으면, 다음에 오는 것들을 확인하세요.
- 배열(array)의 프로토콜 계층구조 : Array Hierachy
- 딕셔너리(dictionary)의 프로토콜 계층구조 : Dictionary Hierachy
- 양방향컬렉션(BidirectionalCollection) : BidirectionalCollection Documentation
이 튜토리얼을 즐겼기를 바랍니다. 자신의 컬렉션을 만드는 것이 가장 흔한 요구사항이 아닐수 있지만, Swift가 제공하는 컬렉션 타입을 더 잘 이해할수 있길 바랍니다.
'Swift > Tip' 카테고리의 다른 글
What’s New in Swift 4.1? (2) | 2018.04.03 |
---|---|
Swift 4에서 JSON 분석하기(Parsing JSON in Swift 4) (0) | 2017.07.11 |
Swift에서 상태 모델링하기(Modelling state in Swift) (0) | 2017.07.11 |
Swift Tutorial: An Introduction to the MVVM Design Pattern (1) | 2017.06.27 |
What’s New in Swift 4? (0) | 2017.06.27 |
Swift에서 사용자 정의 연산자 오버로딩하기(Overloading Custom Operators in Swift) (0) | 2017.05.01 |
Swift3에서 프로토콜 지향 프로그래밍 도입하기(Introducing Protocol-Oriented Programming in Swift3) (0) | 2017.04.25 |
Bond Tutorial: Bindings in Swift (0) | 2017.04.20 |