- Generic은 swift 릴리즈마다 계속 발전해 왔음
- 1.0: Generic Type, Generic Function
- 2.0: Protocol extension, Default, Implementations
- 3.0: Protocol operators, generic type aliases
- 3.1: Nested generic types, Concrete type constrains
- 4.0: Associated type where clauses, Generic subscripts
- 4.1: Conditional conformance, Recursive Constraints
- 그래서 제네릭이 뭔데? 그걸 왜 써야 되는데?
struct Buffer {
var count: Int
subscript(_ index: Int) -> ???
}
- subscript의 반환 타입을 결정할 수가 없다
- Any로 해놓으면, 이게 무슨 타입인지 캐스팅 하기 전까지는 알 수가 없다.
- 원하지 않는 타입이 대입되는 것을 막을 수가 없다.
- Any 타입을 실제 타입으로 바꾸고, 다시 Any로 바꾸는 것도 비용이 든다.
- Any는 indirection이 강제된다.
- 그래서 Generic(parametric polymorphism)을 사용한다.
- 이러면 컴파일 타임에 버퍼의 원소 타입을 결정할 수 있다.
protocol BufferProtocol {
associatedtype Element
}
extension BufferProtocol {
func action() {
...
}
}
struct Buffer<Element>: BufferProtocol {
var count: Int
subscript(_ index: Int) -> Element
}
- 하지만 Element가 너무 일반적이여서, 어떤 연산을 적용하는 게 불가능하다.
- 특정 타입에 대해서만 구현체를 사용할 수 있도록 하자.
- 이를 좀 더 일반화 하면, 특정 프로토콜에 대해서 해당 구현을 사용할 수 있도록 하면 된다.
- 프로토콜 설계
- 구체 타입이 먼저 만들어진다.
- 이후 공통된 부분을 프로토콜로 뺀다.
- 제네릭 인자는 associatedtype으로 지정한다.
- 이후 구현하는 쪽에서 typealias로 이 인자를 공급해준다.
- associatedtype과 제네릭 인자를 동일하게 제공해준다면, typealias를 생략할 수 있다.
- 필요한 프로토콜이 있다면, associatedType 단에서 이를 지정할 수 있다.
- 프로토콜은 계약이다!
- 프로토콜을 채택하려면, 프로토콜의 요구사항을 모두 성실하게 따라야 한다.
- 만약 특정 경우에 좀 더 좋은 구현을 제공할 수 있다면, 오버로딩도 가능하다.
- 만약 하위 타입에서 좀 더 나은 구현을 할 여지가 없다고 판단된다면, extension에서만 구현해두면 된다.
- 프로토콜 상속
- 기존 프로토콜의 기능을 다 가져가면서 다른 기능을 추가하고 싶을 때 사용한다.
- 프로토콜 이름은 최대한 일반적으로 지어야 한다.
- Conditional Conformance: 프로토콜 채택을 특정 조건을 만족하는 경우에만 하는 것
- 이게 추가 되면서, 타입 수를 줄일 수 있었다.(Range vs CountableRange)
- CountableRange가 별도 타입에서 단순히 typealias로 바뀔 수 있었다.
- 다만, 이 경우, 하위 프로토콜을 채택하기 위해서, 상위 프로토콜도 명시적으로 채택하도록 해줘야 한다.
- Recursive constraint
- 프로토콜의 associatedtype이 자기 자신을 채택하는 것
- Collection에서 자신의 부분 집합에도 Collection을 적용하기 위해 사용함
- 원래는 Slice라는 Wrapper를 사용했는데, 일부 Collection은 자신만의 Wrapper(SubString), 혹은 자신과 완전히 동일한 타입(Range)을 사용하길 원함
- 그래서 Collection에 새로운 associatedtype인 SubSequence를 도입
- 근데 이러기 위해서는 SubSequence의 Element와 Index가 원본과 동일해야 한다. -> SubSequence에 Collection 제한을 두고 Element 와 Index가 동일하게 제한을 둔다.
- Slice를 Slice하면?
- 타입이 계속해서 중첩된다!
- 이를 막기 위해서 SubSequence의 SubSequence가 자기 자신이 되도록 제한을 걸어준다.
- 이러한 방식으로 제네릭을 이용해 Divide-And-Conquer를 적용할 수 있다.
- 프로토콜에서의 리스코프 치환 원칙 - 클래스에서만 유효
- 생성자를 프로토콜에 넣는다면, 서브클래스에서는 이를 반드시 required로 구현해야 한다.
- 만약 클래스를 final로 구현한다면, 서브클래스가 없기 때문에 required가 필요없어진다.