Collection
Collection
은 일반적인 “집합 컨테이너”를 묘사하는 프로토콜인데, 실질적으로는 Sequence 프로토콜을 상속하면서 한 가지 개념(기능)을 추가한 것으로 이해할 수 있다. 그것은 임의의 인덱스를 통해서 개별 원소를 액세스할 수 있는 점이다. 따라서 Sequence
와 달리 여러번이고 순회할 수 있고, 순회 시 내부 자료가 소모되지 않는다.
Collection
내의 모든 원소는 집합 구조 내에서 자신의 위치를 특정지을 수 있는 인덱스를 갖는다. 그런 동시에 Sequence
를 상속하기도 했다. (하지만 이것은 Collection
이 되기 위해 Sequence
가 되어야 한다는 뜻은 아니다. Collection
이 되면 자동으로 Sequence
의 동작을 모두 수행할 수 있다는 것이다.) 1
우리는 흔히 배열에서 anArray[2]
와 같은 문법으로 인덱스에 의한 개별 원소를 액세스하고 있다. 이는 전적으로 Collection
의 특징에 기반한다. 인덱스에 의한 랜덤 액세스가 가능하므로 인덱스가 가리키는 위치를 앞으로 뒤로 바꿈으로써 원소의 소모/파괴 없이 여러번 순회할 수 있는 것이 보장된다. 또한 특정 요소를 포함한다/아한다 여부만 확인하는 것이 아니라 특정 요소가 포함되어 있을 때, 즉시 해당 값을 액세스할 수 있는 인덱스를 얻는 것도 가능하다.
콜렉션의 특징은 다음과 같이 정리해볼 수 있다.
Sequence
와 마찬가지로 각 원소들을 순회할 수 있는 연속열의 일종이다.- 모든 원소는 고정된 고유의 위치를 가지며, 이는 인덱스를 통해서 액세스할 수 있다.
- 두 원소간의 거리는 인덱스의 거리로 표현할 수 있고, 두 인덱스간의 범위를 통해 처음과 끝이 아닌 중간 부분의 부분열을 얻을 수 있다.
- 개별 원소를 소모하지 않고 인덱스를 조작하여 순회하는 것이 가능하다.
결국 인덱스에 의한 랜덤엑세스가 가능하다는 것이 Collection
의 가장 큰 특징이라 할 수 있다. 또한 이 때의 인덱스는 전후 관계가 분명해야하므로 Comparable
프로토콜을 만족해야 한다.
프로토콜 적용하기
Collection
프로토콜의 요건은 다음과 같다.
startIndex
,endIndex
프로퍼티를 제공해야 한다.- 인덱스를 이용해 원소를 액세스하는
subscript(_:)
를 구현해야한다. 최소한 읽기용 접근자는 작성해야한다. - 내부에서 인덱스를 증가시키기 위해서
index(after:)
를 구현해주어야 한다.
예를 들어 배열에서는
Index
는Int
타입이며startIndex = 0
,endIndex = self.count
가 된다.subscript(_:)
는Array
에서 구현되었고,index(after:)
는{ $0 + 1 }
이 된다.
사실 프로토콜을 적용함에 있어서 얻을 수 있는 장점은 비소모적인 순회이며, 실제 자료를 조작하는 부분에 있어서는 다음의 기능을 얻게 된다.
index(of:)
,index(where:)
으로 특정 위치를 얻음Range<Index>
를 이용해서 원소 1개가 아닌 부분열을 얻음index(before:)
,index(_:offsetBy:limitedBy:)
와 같은 인덱스 조작 메소드를 얻음
시퀀스와의 차이
예를 들어 시퀀스에서 각 요소의 순회는 makeIterator()
하여 만들어진 이터레이터에 대해 next()
를 호출하여 그 결과가 nil
이 나올 때까지 반복하는 것이다. (그리고 이 과정에서 개별 원소가 소모되어 버릴 수 있다.)
콜렉션은 startIndex
, endIndex
라는 인덱스가 움직일 수 있는 양끝의 범위를 갖는다. 그러면서 다음과 같은 절차를 통해서 실질적으로 next()
를 통해서 시퀀스가 순회하는 것과 동일한 동작을 할 수 있다.
startIndex
에서 시작해self[startIndex]
로 첫 원소를 얻는다.index(after:)
를 이용해서 인덱스를 한 칸 전진한다.self[curentIndex]
를 이용해서 2, 3 번째 원소를 순회한다.- 현재 인덱스가
endIndex
와 같아지면 끝에 도달한 것이므로 순회를 마친다.
이전에 만들었던 Stack
타입을 기억하시는가? 해당 타입을 Sequence
가 되도록 하는 확장은 잊어버리고 그냥 Collection
이 되도록 만들어보자.
protocol StackLike {
associatedtype Element
var top: Element? { get }
var isEmpty: Bool { get }
// mutating 여부는 프로토콜에서 결정해야 한다.
mutating func push(_ value: Element) -> Void
mutating func pop() -> Element
}
struct Stack<T> : StackLike {
var data = Array<T>() // must be `internal`
// conforming StackLike
var isEmpty: Bool { return data.isEmpty }
var top: T? { return data.last }
mutating func push(_ n: T) { data.append(n) }
@discardableResult mutating func pop() -> T {
return data.removeLast()
}
}
extension Stack : Collection {
typealias Index = Int
var startIndex : Int { return 0 }
var endIndex: Int { return data.count - 1 }
func index(after n: Index) -> Index { return n + 1 }
subscript(i: Index) -> T {
return data[endIndex - i]
}
}
위 코드에서 이전 버전과 달라진 점이라고는 확장을 통해서 data
에 직접 액세스하기 위해서 private
스코프 한정자를 뺐다는 점이다. (fileprivate
로 대체할 수 있다.)
프로토콜 적용 부분의 코드를 보면
startIndex
,endIndex
는 외부에서 스택에 접근할 때 사용하는 인덱스 값이다. 이는 0 ~ 데이터의 크기사이가 된다.- 인덱스 증가는
+1
해주는 동작이면 충분하다 - 단 스택은 내부 스토리지를 역순으로 조회해야 하므로
subscript
부분만 주의해주면 된다.
그리고 주목할 점은 이 코드는 Sequence
의 요구조건인 makeIterator()
나 next()
를 구현하지 않았다는 점이다. 다시 다음 코드를 보자.
var stack = Stack<Int>()
(1...5).forEach{ stack.push($0) }
for x in stack { print(x) }
// 5, 4, 3, 2, 1
let doubledCopied = stack.map{ $0 * 2 }
for x in doubledCopied { print(x) }
// 10, 8, 6, 4, 2
그저 Collection
만 충족했을 뿐인데 공짜로 Sequence
에 정의된 메소드들을 그대로 사용할 수 있게 되었다. 여기에는 Stack
이 Collection
을 충족하기 위한 조건을 준비했을 뿐이고, Collection
의 구성원리에서 내부적으로 Sequence
가 필요로 하는 요건을 만족하는 것만으로 Collection
은 자동으로 Sequence
를 만족할 수 있게 되고 따라서 Sequence
가 정의해놓은 디폴트 메소드들을 그대로 적용받게 되었다.
이것이 바로 프로토콜 지향 프로그래밍 패러다임의 강점이다. Objective-C의 프로토콜은 Java의 인터페이스 개념처럼 특정한 동작을 한다는 약속이며, 따라서 특정 메소드를 호출할 수 있다는 근거로 그것을 유사타입으로 사용하는 수준이었지만, Swift의 프로토콜은 표준 구현을 제공함으로써, 프로토콜의 논리적인 기본 요건을 만족하게 될 때 자동으로 해당 요건을 근거로 동작할 수 있는 기능들을 가져다 쓸 수 있게 된다.2
배열은 실질적으로는 Sequence
를 직접 구현했다기보다는 Collection
을 구현함으로써 Sequence
의 요건을 만족하고 그에 따라 유용한 메소드들을 구비한 셈이다. 물론 Array는 Collection
에 정의되지 않은 몇 가지 추가적인 동작을 수행하기도 한다. 사실 이들도 파고들어보면 Collection
을 상속받는 다른 프로토콜들의 도움을 받은 셈이기도 하다.
목차
- Array – Array 타입, 생성과 조작
- Array – Sequence 프로토콜
- Array – Collection 프로토콜
- Array – ArraySlice
- Array – NSArray
- 하지만
Collection
은Sequence
의 기본 구현을 그대로 사용하는 것이 아닌 별도의 디폴트 구현을 가지고 있다. (이터레이터를 사용하지 않고 그저 인덱스만을 이용해서 구현한다.) 다만 콜렉션이 가지는 특성들이 시퀀스의 그것을 모두 포함하기 때문에, 인덱스의 범위와 인덱스 전진 연산만으로 시퀀스가 정의한 일반 메소드 명세들을 모두 구현할 수 있다는 사실 때문에 상속관계로 표현된다. - 하스켈에서 이것은 타입클래스를 통해 이미 선보인바 있는 기능이다.