- 4.2 릴리즈 시점
- Swift에선 Generic을 이용해서 표현력을 향상시켰음
- What are generics?
- Protocol design
- Protocol inheritance
- Conditional conformance
- Classes and generics
예시로 표준 라이브러리의 Array와 유사한 Buffer
라는 타입을 들어보자.
struct Buffer {
var count: Int
subscript(at: Int) -> ??? {
// get/set from storage
}
}
???
- 인덱스에 대한 반환 타입을 뭐로 해야할까?
var words: Buffer = ["subtyping","ftw"]
// I know this array contains strings
words[0] as! String
// Uh-oh, now it doesn’t!
words[0] = 42
➡️ 사용 편의성과 정확성 그리고 성능상의 이유로 해결하고자 함
Swift에서 제네릭이라고 부르는 다른 용어
Any 대신에 Element
라는 이름으로 타입 매개변수를 정해주자
타입의 제네릭 매개변수 이므로 매개변수 다형성(Parametric Polymorphism)이라는 용어를 가짐
struct Buffer {
var count: Int
subscript(at: Int) -> Element {
// get/set from storage
}
}
var words: Buffer<String> = ["generics","ftw"]
words[0]
덧셈하는 메서드를 Buffer에 추가하고 싶다!
extension Buffer {
func sum() -> Element {
var total = 0
for i in 0..<self.count {
total += self[i]
}
return total
}
}
⚠️ 모든 타입에서 가능하지 않기 때문에 컴파일 에러 발생
이 방식을 사용하려면 Element가 가져야 하는 조건을 컴파일러에게 더 많이 알려야 함
extension Buffer where Element == Int {
extension Buffer where Element : Numeric {
ex) Buffer, Array, Data, String, Dictionary..
protocol Collection {
}
protocol Collection {
associatedtype Element
}
protocol Collection {
associatedtype Element
associatedtype Index
subscript(at: Index) -> Element
func index(after: Index) -> Index
var startIndex: Index { get }
var endIndex: Index { get }
}
extension Collection where Index: Equatable
protocol Collection {
associatedtype Element
associatedtype Index : Equatable
}
프로토콜에 요구사항을 추가하고 extension을 통해 기본 구현을 추가하는 것을 사용자 정의 지점(Customization Points)이라고 함
-> 클래스의 상속 및 override와 동일한 이점을 얻을 수 있는 강력한 방법
-> 상속과 달리 Enum과 Struct에도 적용 가능 👍
때로는 타입을 분류하기 위해 단일 프로토콜 이상이 필요
프로토콜 상속은 Swift 시작부터 있었음
lastIndex(where:)
- 마지막 요소를 얻고 싶을 때 뒤에서부터 하면 되지만 그럴 수 없음suffle
- 임의로 섞고 싶을 때 mutation이 필요하고 그럴 수 없음Collection 프로토콜이 잘못된 것이 아니라 이러한 알고리즘을 하기 위해서는 많은 것이 필요하며 이것이 프로토콜 상속의 요점
protocol BidirectionalCollection: Collection {
func index(before idx: Index) -> Index
}
BidirectionalCollection = Collection을 상속받은 프로토콜
Shuffle 과정
1) 첫 번째 요소에 대한 인덱스로 시작
2) 첫 번째 이후에서 임의의 다른 인덱스를 선택
3) 이 두 요소를 교환
4) 두 번째(그 다음 요소) 인덱스를 선택하고 교환
5) 마지막 요소까지 위 과정을 반복
extension ShuffleCollection {
mutating func shuffle() {
let n = count
guard n > 1 else { return }
for (i, pos) in indices.dropLast().enumerated() {
let otherPos = index(startIndex, offsetBy: Int.random(in: i..<n)) // 시작 인덱스부터 특정 위치의 인덱스를 얻는 작업
swapAt(pos, otherPos) // 두 인덱스 위치를 교환하는 작업
}
}
}
ShuffleCollection
이라는 새로운 타입을 만들지 말자하나의 알고리즘을 표현하는 프로토콜을 만들면 의미없는 프로토콜이 많아지게 됨
RandomAccessCollection
- 인덱스를 빠르게 이동하면서 컬렉션을 건너뛸 수 있게 함
MutableCollection
- 임의 접근을 제공
extension RandomAccessCollection where Self: MutableCollection {
mutating func shuffle() {
// 알고리즘 구현
}
}
➡️ RandomAccessCollection이 MutableCollection을 준수하게 하면 shuffle
알고리즘을 구현할 수 있음
준수하는 타입이 많고 제네릭 알고리즘이 많으면 프로토콜 계층 구조가 형성됨
이러한 계층구조가 너무 크거나 너무 세분화되어도 안됨
Swift의 새로운 기능
컬렉션에 대해 특정 인덱스 범위로 slice를 만들 수 있음
struct Slice<Base: Collection>: Collection { ... }
Slices는 제네릭 어댑터 타입 -> 기본 컬렉션에서 수행할 수 있는 작업을 Slice에 수행할 수 있다
반드시 Slice가 BidirectionalCollection이라는 보장이 없기 때문에 lastIndex(before:)
를 사용할 수 없음
index
가 없는데? -> Slice가 아닌 Base가 BidirectionalCollection을 준수하도록 변경➡️ 이것이 Conditional Conformance
/ 다시 보고 설명좀
typealias는 Range를 셀 수 있게 만드는 모든 추가 요구사항을 적용
기본 Range 타입의 대체 이름일 뿐
다루고 있는 타입 세트를 단순화하는 데 도움
Range와 같은 기존 핵심 타입을 보다 구성 가능하고 유연하게 만듦
프로토콜과 관련 타입(associatedtype) 간의 관계를 설명
동일한 프로토콜을 언급하는 프로토콜 내의 제약
protocol Collection {
// ...
associatedtype SubSequence: Collection
}.
SubSequence 자체가 Collection
왜 이런게 필요할까? 🧐
➡️ 정렬된 컬렉션에서 정렬을 유지하며 새 값을 삽입할 인덱스 찾기
삽입 지점 찾기는 이진 탐색
으로 구현됨
분할 정복 알고리즘
각 단계에서 문제 크기를 줄일 수 있는 결정을 내려 빠른 속도로 찾음
1) 중간 값을 삽입하려는 값과 비교
2) 삽입 하려는 값이 더 크다면 절반 후반부에서 다시 검색 (만약 작다면 전반부)
3) 적절한 삽입 지점을 찾을 때까지 계속해서 반으로 나누며 반복
extension RandomAccessCollection where Element: Comparable {
func sortedInsertionPoint(of value: Element) -> Index {
if isEmpty { return startIndex }
let middle = index(startIndex, offsetBy: count / 2) // 중간 요소의 인덱스를 찾기
if value < self[middle] { // 중간 값 앞에 오는지 확인
return self[..<middle].sortedInsertionPoint(of: value)
} else {
return self[index(after: middle)...].sortedInsertionPoint(of: value) // 크다면 중간 뒤의 인덱스에서 컬렉션 Slice를 가져와서 다시 재귀적으로 호출
}
}
}
self[index(after: middle)...]
에서 일부 컬렉션은 Slice 타입을 원하지 않을 수 있음슬라이싱 작업 = Range 범위의 새로운 인스턴스를 반환
컬렉션을 준수하는 다양한 타입 간의 변형을 캡처하기 위해 컬렉션 프로토콜에 새로운 요구사항을 도입할 수 있다
extension Collection {
subscript (bounds: Range<Index>) -> Slice<Self> {
return Slice(base: self, bounds: bounds)
}
}
--->
protocol Collection {
// ...
associatedtype SubSequence
subscript (range: Range<Index>) -> SubSequence { get }
}
슬라이싱을 제공하는 subscript
를 컬렉션 프로토콜의 요구 사항으로 가져옴
=> String과 Range 모두 요구사항을 충족할 수 있음
typealias SubSequence = Substring
typealias SubSequence = Range<Bound>
protocol Collection {
// ...
associatedtype SubSequence = Slice<Self>
subscript (range: Range<Index>) -> SubSequence { get }
}
extension Collection {
subscript (bounds: Range<Index>) -> Slice<Self> {
return Slice(base: self, bounds: bounds)
}
}
SubSequence를 사용하지 않으려는 다른 컬렉션의 경우 Slicing의 기본 구현을 제공할 수 있음
extension Collection where Self.SubSequence == Slice<Self> {
where 절을 통해 기본 구현의 적용 가능성을 제한할 수 있음
subscript
사용우리의 목표는 컬렉션 프로토콜에 대한 분할 정복 알고리즘 작성하기
재귀적인 알고리즘
extension RandomAccessCollection where Element: Comparable {
func sortedInsertionPoint(of value: Element) -> Index {
if isEmpty { return startIndex }
let middle = index(startIndex, offsetBy: count / 2)
if value < self[middle] {
return self[..<middle].sortedInsertionPoint(of: value)
} else {
return self[index(after: middle)...].sortedInsertionPoint(of: value)
// -> SubSequence인 Slice 형성
// 이후 해당 Slice에서 삽입 지접을 재귀적으로 호출
// (1) 반환된 SubSequence 타입이 Collectin인 경우에만 의미가 있음
// (2) 사용하는 Element도 동일해야 함
// (3) 결과로 반환되는 인덱스 역시 현재 Collection에서 유효한 인덱스여야 함
}
}
}
1) Collection의 SubSequence 자체가 Collection이도록 제약
2) SubSquence의 Element가 Collection의 Element와 같도록 제약
3) Index 역시 같도록 제약
SubSequence의 SubSequence도 가능해짐 -> SubSequence의.SubSequence의.SubSequence의...
Slice의 Slice를 새롭게 적용하는 것이 아닌 같은 Index를 사용하므로 새로운 Index를 적용하는 방식
방식 1) Slice의 [i, j] -> Slice.Slice의 [i2, j2] -> Slice.Slice.Slice의 [i3, j3] ... ❌
방식 2) Slice의 [i, j] -> Slice의 [i2, j2] ✔️
Collection이어야 한다고는 했지만 index(offsetBy:)
작업을 하려면 RandomAccessCollection이어야 함
protocol BidirectionalCollection: Collection
where SubSequence: BidirectionalCollection {
// ...
}
protocol RandomAccessCollection: BidirectionalCollection
where SubSequence: RandomAccessCollection {
// ...
}.
재귀 제약 조건(Recursive Constraints)과 조건부 적합성(Conditional conformance)은 모두 프로토콜 계층 구조를 추적하는 경향이 있음
associatedtype 기본값이 계층 구조 내의 모든 프로토콜에 대해 작동하는 이 방법은 응집력 있는 디자인의 좋은 예시
associatedtype 및 where 절을 함께 사용하면 분할 정복 알고리즘을 일반 코드로 자엽스럽게 표현하는 요구사항을 작성하는 데 도움
Swift는 다중 패러다임 언어
Swift는 OOP도 지원 => 제네릭과 클래스의 상호 작용에 대해 알아보자
Vehicle - 상위 클래스
Taxi, PoliceCar - Vehicle을 상속받은 하위 클래스
drive() - 상위 클래스의 메서드
리스코프 치환 원칙
리스코프 치환 원칙
상위 클래스는 상위 타입을 참조하는 하위 클래스로 대체할 수 있어야 한다
Drivable - 프로토콜
Vehicle - Drivable 채택
sundayDrive() - Drivable의 기본 구현 메서드
Drivable을 채택한 타입이 drive
를 지원하고 sundayDrive()
를 사용할 수 있음
하위 클래스에서 새로운 요구 사항을 추가하는 경우가 있음 -> initializer
1) 왜 Self를 사용할까?
2) 구현 방법
init
을 호출* self -> 해당 타입의 인스턴스
상속되면서 다른 init을 호출하게 될 수도 있는거 아닌가? 🧐
Vehicle protocol을 채택하는 시점에서 init
구현 문제를 알려줌
-> required
로 표시하고 모든 하위 클래스에서 구현되어야 함
final
현재 클래스가 상속되지 않음을 의미를 나타내는 키워드
하위 클래스가 없음을 알기에 required
을 붙일 필요 없음
Swift 제네릭의 기본 아이디어
= 정적 타입을 유지하면서 코드를 재사용할 수 있는 기능 제공
-> 올바른 프로그램을 더 쉽게 작성, 효율적으로 실행되는 프로그램으로 컴파일
리스코프 치환 원칙
을 적용하기