Sequence는 한 번에 하나씩 iteration할 수 있는 리스트를 말합니다
Sequence의 element를 iterate over하는 가장 일반적인 방법은 for-in
루프를 사용하는 것입니다
let oneTwoThree = 1...3
for number in oneTwoThree {
print(number)
}
// Prints "1"
// Prints "2"
// Prints "3"
겉보기에는 간단하지만, 이 기능을 통해 어떤 Sequence에서든 매우 많은 동작을 수행할 수 있습니다
예로, 어떤 Sequence가 특정 값을 포함하고 있는지 체크하기 위해
Sequence의 처음부터 마지막 element까지 차례대로 매칭을 시도해볼 수 있습니다
아래 예제는 배열에 특정 곤충이 있는지 확인합니다
let bugs = ["Aphid", "Bumblebee", "Cicada", "Damselfly", "Earwig"]
var hasMosquito = false
for bug in bugs {
if bug == "Mosquito" {
hasMosquito = true
break
}
}
print("'bugs' has a mosquito: \(hasMosquito)")
// Prints "'bugs' has a mosquito: false"
Sequence 프로토콜은 다양한 동작들을 default implementation으로 제공합니다
이 동작들은 Sequence의 각 값들에 순차적으로 접근하는 기능을 이용합니다
위 예제를 좀 더 명확하고 깔끔한 코드로 만드려면, contains()
메서드를 사용할 수 있을 것입니다
(for문으로 하나씩 iteration하지 않고 포함여부를 체크할 수 있다)
이는 모든 Sequence가 가지는 메서드입니다
if bugs.contains("Mosquito") {
print("Break out the bug spray.")
} else {
print("Whew, no mosquitos!")
}
// Prints "Whew, no mosquitos!"
Sequence 프로토콜은 iteration을 하면서 destructive한 행위를 하던지 말던지 신경쓰지 않습니다
(=요구사항이 없습니다)
결과적으로, 같은 Sequence에 대해 for-in
루프를 여러 번 돌리는 상황에서
iteration이 이전 루프의 끝에서부터 resume한다던지
처음부터 restart한다던지 할 것을 확신하면 안된다는 말입니다
for element in sequence {
if ... some condition { break }
}
for element in sequence {
// No defined behavior
}
위 예시에서, sequence
에 대해 루프를 두 번 돌리는 행위가
iteration이 consumable하여 두번째 루프가 첫번째 루프의 끝에서부터 resume한다던지 /
두번째 루프가 collection처럼 처음부터 시작할 것이라던지 하는 확신에 찬 예상을 하면 안됩니다
(어찌될지 알 수 없음)
Collection 프로토콜을 준수하지 않는 Sequence는 두번째 루프에서 어떻게든 변경되어도 무관합니다
만약, 이런 경우에서 iteration이 destructive한 동작을 하지 않도록 금지시키려면
Collection
프로토콜을 준수시켜야 합니다
당신이 만든 custom 타입이 Sequence 프로토콜을 준수하도록 하면 많은 유용한 동작들이 가능해집니다
(ex. for문 루프 / contains 등)
Sequence 프로토콜을 준수시키려면, iterator를 return하는 makeIterator()
메서드를 추가해야 합니다
만약 당신의 타입 자체가 iterator로 동작할 수 있다면, 대안이 있습니다
바로, IteratorProtocol
을 채택하고 그 요구사항을 준수시키면 makeIterator()
메서드가 default implementation으로 제공되면서 대체할 수 있습니다
아래 예제가 바로 그런 예시입니다
struct Countdown: Sequence, IteratorProtocol {
var count: Int
mutating func next() -> Int? {
if count == 0 {
return nil
} else {
defer { count -= 1 }
return count
}
}
}
let threeToGo = Countdown(count: 3)
for i in threeToGo {
print(i)
}
// Prints "3"
// Prints "2"
// Prints "1"
Sequence는 자신의 iterator를 제공하는 것은 O(1)으로 제공해야 합니다
그리고, iteration over는 일반적으로 O(n)으로 고려되어야 합니다
element access에 대한 별다른 요구사항이 없기 때문입니다
Collection + Sequence가 제공하는 유용한 메서드들
- Collection.first
- Collection.firstIndex(of:)
- Collection.startIndex
- Collection.prefix()
- Collection.suffix()
- Collection.indices
- Collection.max()
Collection은 Swift 표준 라이브러리에서 매우 광범위하게 사용됩니다
(Array / Dictionary 등)
Collection은 Sequence 프로토콜로부터 상속받은 기능들에 더해,
특정 position에 있는 element로의 접근을 제공합니다
아래와 같이, String의 첫 번째 word를 출력하고 싶다면,
기준이 될 index를 찾아서 substring을 만들 수 있습니다
let text = "Buffalo buffalo buffalo buffalo."
if let firstSpace = text.firstIndex(of: " ") {
print(text[..<firstSpace])
}
// Prints "Buffalo"
유효한 index를 사용한 subscripting
당신은 유효한 index를 사용한 subscript로 collection의 element에 접근할 수 있습니다
유효한 index는 Collection의 endIndex
프로퍼티 '이전'까지입니다
이 프로퍼티는 end의 '다음' index이기에 어떠한 element와도 대응되지 않습니다
아래는 Collection인 String을 subscript를 사용해 첫번째 Character로 접근하는 예제입니다
let text = "Buffalo buffalo buffalo buffalo."
let firstChar = text[text.startIndex]
print(firstChar)
// Prints "B"
default implementation
Collection 프로토콜은 subscript를 사용한 접근에 기반한 많은 default implementation을 제공합니다
아래와 같이, first
프로퍼티를 통해 첫번째 element에 접근할 수 있습니다
(collection이 비었으면 nil을 반환)
print(text.first)
// Prints "Optional("B")"
startIndex
/ endIndex
를 사용하여 유효한 index를 찾을 수 있습니다
index가 invalid될 수 있다
이런 index 프로퍼티들은 어디 저장해놓더라도
Collection이 mutating 동작을 하면서 invalid 해질 수 있습니다
mutable collection에서 index invalidation에 대한 것은
MutableCollection
/ RangeReplaceableCollection
프로토콜 참고
ranged subscript를 통해 collection의 slice에 접근할 수 있습니다
(ex. prefix
/ suffix
)
slice는 collection 요소의 일부를 포함할 수 있고,
원본 collection의 semantic을 공유합니다 (?)
아래 예제는 prefix(while:)
메서드로 String의 일부를 추출합니다
let firstWord = text.prefix(while: { $0 != " " })
print(firstWord)
// Prints "Buffalo"
아래와 같이 ranged subscript로도 동일한 slice를 추출할 수 있습니다
if let firstSpace = text.firstIndex(of: " ") {
print(text[..<firstSpace]
// Prints "Buffalo"
}
collection과 그의 slice는 같은 index를 공유합니다
이게 무슨 말이냐면, 같은 index에 대응되는 element가 같다는 의미입니다
예제1
아래 예제를 보면 이해가 수월합니다
var collection = [1,2,3]
var slice = collection.suffix(2) // [2,3]
print(collection[2])
// Prints 3
print(slice[2])
// Prints 3
slice는 element가 2개이지만 index:2로 세번째 element에 접근할 수 있습니다
예제2
아래 예제에서, index를 공유한다는 특징으로 인해 복잡해진 코드를 확인할 수 있습니다
전체 day 중 second half에서의 max 값이 궁금한 상황입니다
var absences = [0, 2, 0, 4, 0, 3, 1, 0]
let secondHalf = absences.suffix(absences.count / 2)
if let i = secondHalf.indices.max(by: { secondHalf[$0] < secondHalf[$1] }) {
print("Highest second-half absences: \(absences[i])")
}
// Prints "Highest second-half absences: 3"
index 공유 특징으로 인해, secondhalf를 0번부터 사용한게 아니라,
indices
라는 프로퍼티로 영역을 뽑아서 사용해야 했습니다
주의할 점은, 자신이 갖고 있지 않은 index의 element에 접근한다고 해서
원본으로 찾아가는게 아니라 런타임 에러납니다
(대체 semantic이란게 뭘까..)
slice는 COW 최적화의 대상으로, 참조 타입일 수도, 값 타입일 수도 있습니다
기본적으로 slice는 원본 Collection의 메모리를 참조하는 형태로, 원본 data를 공유합니다
그러다가, 원본 혹은 slice를 바꾸려는 시도가 들어오면
원본을 copy하여 별도의 메모리를 할당받게 됩니다
예로, 아래와 같이 원본의 마지막 element를 업데이트하더라도 slice는 변경되지 않습니다
absences[7] = 2
print(absences)
// Prints "[0, 2, 0, 4, 0, 3, 1, 2]"
print(secondHalf)
// Prints "[0, 3, 1, 0]"
이전에 확인했듯이, Sequence는 iteration이 진행되면서 consume될 수 있습니다
반면, Collection은 multipass일 것을 보장받습니다
(용어가 이상한데.. 아무튼 for문이 시작될 때 루프를 얼마나돌지 index들을 저장하고 시작하기 때문에
중간에 destructive한 동작이 있어도 루프 자체는 끝까지 돈다는걸 말하는 듯 합니다)
게다가, collection의 index들은 유한한 범위를 형성합니다
이 사실은 어떤 collection이든 안전하게 sequence 동작을 하도록 보장해줍니다
iteration을 position index기반으로 돌리나 / iterator로 돌리나 산출되는 element들은 같습니다
이를 증명하는 예제를 아래에서 확인할 수 있습니다
let word = "Swift"
for character in word {
print(character)
}
// Prints "S"
// Prints "w"
// Prints "i"
// Prints "f"
// Prints "t"
for i in word.indices {
print(word[i])
}
// Prints "S"
// Prints "w"
// Prints "i"
// Prints "f"
// Prints "t"
starteIndex
/endIndex
프로퍼티 정의
- element로 접근할 subscript 정의 (subscript는 최소 read-only는 되어야 한다)
index(after:)
메서드 정의 (현재 index의 다음 index를 return하는 메서드)
startIndex/endIndex/subscript로 접근하다보니 O(1)으로 예상됩니다
만약 O(1)을 보장하지 않는 Collection이라면 시작시점을 반드시 document해야 합니다
왜냐하면, 어지간한 Collection 동작들의 자체 성능을 보장하려면 subscripting 성능이 일단 O(1)이어야 하기 때문입니다
(depend on O(1) subscripting performance)
어떤 Collection들은 index의 타입에 영향을 받습니다
RandomAccessCollection
특정 index element에 접근하고 / 두 index 사이의 element의 개수를 세는데 있어 O(1)의 성능을 보장하는 프로토콜입니다
당연한거 아닌가?하고 생각할 수 있지만, linked list처럼 n번째 element를 읽기위해 첫번째 element부터 쭉 순회해야 하는 Collection도 있습니다
RandomAccessCollection의 subscripting과 count 프로퍼티 계산은 O(1)으로 해결됩니다
BidirectionalCollection
반대로, forward 혹은 bidirectional collection은 count를 계산하기 위해 전체 Collection을 순회해야 하므로 O(n) 성능을 보입니다