-
Sequence를 알아보자 🤿Swift 2024. 1. 29. 19:06
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 Swift의 Sequence에 대해 학습해보겠습니다 🙋🏻
Sequence에 대해 직접적은 아니더라도 모두 개발하시면서 알게 모르게 스며들어 있습니다!
Array, Dictionary, Set 같은 Collection 타입들을 자주 사용하실텐데요.
이 컬렉션 타입들은 Sequence 프로토콜을 채택하고 있어요.
protocol Collection<Element> : Sequence
그렇기에 이 시퀀스가 대체 어떤거고 기본 컬렉션 타입들을 사용하지 않고 시퀀스 타입을 만들어보는것들에 대해 알아보겠습니다.
Sequence
순서라는 뜻을 가진 이 시퀀스 프로토콜은 해당 요소에 대해 순차적이고 반복적인 액세스를 제공하는 유형입니다.
아래와 같은 정의를 가져요.
protocol Sequence<Element> public protocol Sequence { /// A type representing the sequence's elements associatedtype Element where Self.Element == Self.Iterator.Element /// A type that provides the sequence's iteration interface and /// encapsulates iths iteration state. associatedtype Iterator: IteratorProtocol /// Returns an iterator over the elements of this sequence. func makeIterator() -> Self.Iterator }
이번에 공식문서를 보다 visionOS에 대해서도 추가된것이 신기했어요!
시퀀스 프로토콜은 Element라는 네이밍으로 제네릭 요소를 전달 받아 사용합니다.
시퀀스는 한번에 하나씩 실행할 수 있는 값의 목록으로 가장 일반적으로 사용하는 방법으로는 우리가 흔히 익숙한 for-in 루프를 활용하는거에요.
let oneTwoThree = 1...3 for number in oneTwoThree { print(number) } // Prints "1" // Prints "2" // Prints "3"
단순하지만 이 기능을 사용하면 모든 시퀀스에서 수행할 수 있는 수많은 작업에 액세스할 수 있습니다.
예를들어서 시퀀스에 특정 값이 포함되어 있는지 확인을 하거나 찾거나 등 각 값들을 순차적으로 테스트할 수 있죠.
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"
시퀀스 프로토콜은 이런 순차적인 액세스에 의존하는 많은 대중적인 작업에 대해 기본 구현들을 제공해줍니다.
위처럼 수동으로 반복을 통해 찾을수도 있지만, 아래와 같이 내장된 메서드들을 사용할 수도 있습니다.
if bugs.contains("Mosquito") { print("Break out the bug spray.") } else { print("Whew, no mosquitos!") } // Prints "Whew, no mosquitos!"
시퀀스를 가지고 사용할 수 있는 내장 메서드들은 아주 익숙한것들이 많습니다.
흔히 사용되는 contains, allSatisfy, max, min처럼 요소를 탐색하기 위해서 사용할 수도 있으며,
filter, map, reduce, sort 등 요소를 제외하거나 시퀀스를 변환, 정렬하는 메서드들도 모두 포함되어 있죠!아마 여러분들이 컬렉션 타입을 이용할때 자주 사용되는 메서드들이 이 시퀀스 프로토콜을 통해 이뤄진다고 볼 수 있습니다.
정말 너무너무 많은 메서드들이 제공되니 꼭 공식문서를 한번 참고해보면 좋을것 같습니다 😃
또한, 시퀀스를 순회하는 동안에 시퀀스 내의 요소가 변경되거나 소멸되지 않습니다.
예시로 for-in 루프를 사용해 시퀀스를 한번 순회 후 다시 순회를 시작하면 처음부터 시작될 수도 있고 중간부터 시작될 수도 있습니다.
이는 시퀀스 프로토콜이 이에 대해 특별히 정의하지 않았기 때문이죠.
for element in sequence { if ... some condition { break } } for element in sequence { // No defined behavior }
위 코드의 경우 시퀀스가 소비 가능하며 반복을 재개할 것이라고 가정하거나, 시퀀스가 컬렉션이며 반복을 첫번째 요소부터 재시작할거라고 가정할 수 없습니다!
컬렉션은 아니지만 시퀀스에 준수하는 타입은 두번째 for-in 루프에서 임의의 요소 시퀀스를 생성할 수 있다는 점이죠.
시퀀스 프로토콜을 따르는 컬렉션 타입들을 사용할 수도 있겠지만, 자체적인 커스텀한 시퀀스 타입을 만들어낼 수도 있습니다.
한번 커스텀한 시퀀스 타입을 만들어볼까요?
커스텀한 시퀀스 타입 생성하기
두가지만 기억하면 됩니다.
1️⃣ Sequnece 프로토콜을 채택한다.
2️⃣ IteratorProtocol 프로토콜을 채택하고 next 메서드 구현하기
그럼 공식문서의 예제를 통해 볼까요?
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"
Countdown이라는 타입은 Sequence와 IteratorProtocol을 채택하고 있습니다.
앞서 Sequence 프로토콜의 정의를 볼때 makeIterator 메서드가 요구되는걸 볼 수 있었어요.
func makeIterator() -> Self.Iterator
해당 메서드는 시퀀스 요소에 대한 Iterator를 반환해줍니다.
그렇기에 해당 메서드를 구현해줘야하지만 우리는 IteratorProtocol을 해당 타입에서 같이 분리하지 않고 채택하고 있음으로 IteratorProtocol 필수 메서드인 next 메서드만 구현해주면 되는것이죠.
정리하자면, 시퀀스 타입을 만들기 위해서는 시퀀스 프로토콜을 채택할때 makeIterator 메서드를 구현해줘야하지만, IteratorProtocol을 같은 타입에서 동시에 준수하기에 별도로 구현하지 않아도 됩니다.
필요한건 next 메서드만이며 해당 메서드는 Iterator가 다음 요소를 생성하거나 반복이 끝났음을 나타내는 로직 메서드입니다.
그렇기에 이렇게 구현된다면 해당 타입을 for-in 반복문에서도 사용할 수 있는것이죠~!
그렇기에 다시 위 예제 코드를 살펴보면 두 프로토콜을 동시에 준수하고 있고 next 메서드를 구현해줬습니다.
해당 메서드에서는 탈출 조건을 꼭 걸어줘야합니다.
그렇지 않으면 무한 루프를 돌것입니다 😵💫
가끔 이 두 프로토콜 채택을 별개로 타입으로 분리할때도 필요하겠죠?
struct Countdown: Sequence { let start: Int func makeIterator() -> CountdownIterator { return CountdownIterator(self) } } struct CountdownIterator: IteratorProtocol { let countdown: Countdown var times = 0 init(_ countdown: Countdown) { self.countdown = countdown } mutating func next() -> Int? { let nextNumber = countdown.start - times guard nextNumber > 0 else { return nil } times += 1 return nextNumber } }
그렇다면 이렇게 나눠줄 수 있습니다.
차이점은 Countdown 타입은 Sequence 프로토콜만 채택하고 있으니 makeIterator 메서드를 구현해줘야 합니다.
Iterator 타입을 반환하여야하죠.
그리고 분리한 CountdownIterator 타입은 IteratorProtocol을 채택하며 필수 구현 메서드인 next 메서드를 구현해줍니다.
그리고 이 타입을 Coundown의 makeIteractor 메서드에서 반환 타입으로 사용하면 됩니다 😀
마무리
간단하죠..!?
그냥 막연히 알거나 개발속 깊이 스며들어 있었는데 어떤건지 잘 몰랐던 부분들을 다시 한번 짚어봤어요!
링크드리스트와 같은것들을 직접 구현할때도 활용될 수 있지 않을까 생각이 들더라구요ㅎㅎ
레퍼런스
'Swift' 카테고리의 다른 글
What's new in Swift 5.10 (64) 2024.03.21 New access modifier - package (78) 2024.03.04 Assertions & Preconditions (115) 2024.01.25 추상화와 다형성 (54) 2023.12.11 rethrows로 에러를 다시 던져보자 🥏 (40) 2023.11.03