제네릭은 클래스나 함수 같은 타입에서 특정 타입에 구애받지 않고 유연하게 설계를 도와주는 기능이다.
Swift에서의 제네릭은 Any나 AnyObject와 비슷하게 다양한 타입을 처리할 수 있지만, 타입 안전성을 유지하는 점이 다르다.
Any는 모든 타입을 허용하지만, 타입 안전성이 떨어질 수 있고, AnyObject는 클래스의 인스턴스만 허용한다.
하지만 제네릭을 사용하면 모든 타입을 허용하면서도, 필요에 따라 특정 타입이나 프로토콜을 준수하는 타입만을 사용할 수 있도록 제약을 추가할 수 있습니다.
즉, 제네릭은 특정 타입에 얽매이지 않는 유연함을 제공하면서도, 상황에 맞게 타입 제약을 통해 더 안전하고 구체적인 설계를 할 수 있습니다.
클래스나 구조체같은 타입이나 프로토콜, 함수에서도 사용이 가능하다.
기본적으로 <>각괄호안에 임의 이름을 넣어서 사용한다.
이 이름을 타입 파라메터라고 한다.
class someClass<T>{
init(){
//(대충 초기화 하는 내용)
}
//(대충 클래스 내부 프로퍼티)
}
다음과 같은 형태로 사용한다.
사용하는 종류별 상세 구현형태는 아래에서 자세히 다루겠다.
위 코드에서 처럼 <>각 괄호 안에 들어가는 이름을 타입 파라메터라고 하는데 이름을 짓는데도 일종의 국룰이 있다.
제네릭에는 보통 타입에 관한 정보를 나타내기 때문에
다음과 같이 사용하는 정보를 의미하는 단어의 제일 앞 알파벳 1자를 대문자로 적어서 표기한다.
<T> <U> <V>
하지만 swift에서 Element나 Key, Value같은 의미가 있는 관계의 경우 단어 자체로 표기해놓은 경우도 많다.
func swapTwoStrings(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
만약 두개의 파라메터를 받아서 서로의 값을 swap하는 함수가 있을때 제네릭이 없다면 int, double, string등등 각 타입별로 한개씩 n개의 함수를 만들어야 할것이다.
제네릭을 이용하면 다음과 같이 사용이 가능하다.
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
제너릭 버전은 Int, String, 또는 Double 와 같은 실제 타입 이름 대신에 T 라는 임의의 이름을 사용한다 T는 정해진건 아니고 아무 문자열이나 가능하다
이번엔 예시로 간단하게 구현한 Stack을 이용하겠다.
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
다음과 같이 int만 사용이 가능한 Stack이 있을때
제네릭으로 변경하면 다음과 같다.
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
사용할때 구조체 이름 뒤의 <>각괄호안에 사용하고자 하는 타입을 명시하면 된다.
사실 형태는 구조체와 동일하다.
다음은 이전의 Stack을 클래스로 구현한 예시이다.
class Stack<Element> {
var items: [Element] = []
func push(_ item: Element) {
items.append(item)
}
func pop() -> Element {
return items.removeLast()
}
}
맨 처음에 말했듯이 제네릭이 Any와 다른점은 제약사항을 걸어서 좀더 안정성있게 다양한 타입을 받아들일 있다고 했었는데
실제로 Swift에 정의된 컬렉션 타입들중에 Dictionary를 예로들자면 이 딕셔너리의 키로 들어올 수 있는 데이터는 hashable프로토콜을 준수하는 데이터만 사용이 가능하다.
이처럼 안정성을 위해서 타입을 제약하는 일이 흔하다는것을 알고 형태를 알아보자.
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
someT는 SomeClass의 하위클래스의 인스턴스 여야 하고
someU는 SomeProtocol을 준수하는 인스턴스 여야 한다고
제약사항을 설정한 코드이다.
Associated Type은 프로토콜에서 제네릭을 이용하기 위한 방법이다.
아직 프로토콜을 잘모른다면 프로토콜을 보고오도록 하자.
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
확장에 사용된 제네릭에 제약사항을 추가할 수 있다.
protocol Container {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
이 코드는 제네릭으로 데이터를 받되 Equatable프로토콜을 준수하는 데이터만 올 수 있다고 제약사항을 설정 하고 있다.
Item: Equtable 이렇게 하면 앞서 제네릭에 타입제약을 걸었던 것처럼 제네릭에 준수해야 하는 프로토콜을 명시 할 수있다.
앞서 :콜론을 사용한 제약사항은 해당 클래스의 하위클래스이거나 프로토콜을 준수하기를 명시하는것이었는데 where은 좀더 상세하게 제약을 걸 수 있다.
명심해야 할것은 :과 where은 별개의 개념이 아니다 where에서 :콜론을 이용할 수 있다. 제약조건의 상위 포지션이라고 생각하면 된다.
func areItemsEqual<T, U>(item1: T, item2: U) -> Bool where T: Equatable, T == U {
return item1 == item2
}
이런식으로 제네릭 함수에 where을 사용해서 T는 Equatable을 준수하며 T와 U는 같은 경우에만 함수를 사용할 수 있게 설정하였다.
if문과 비슷하지만 다른점은 where을 이용하면 swift에서 컴파일 자체를 허용하지 않는다.
일단 extension에 대해서 모른다면 extension글을 보고 오는것을 추천한다.
extension someClass where Element: Equatable {
//클래스 내부의 저장프로퍼티인 topItem과 파라메터로 넘겨 받은 item을 비교하는 함수
func isTop(_ item: Element) -> Bool {
return topItem == item
}
}
위 코드는 someClass에서 제네릭인 Element를 확장하여 Equatable을 준수하는 데이터만 처리 할 수 있도록 확장하고 있다.
제네릭을 추가하는데 아니고 이미 정의된 제네릭에 제약조건을 추가하는 코드이다.
앞서 프로토콜에서 제네릭을 이용하려면 associatedtype을 이용한다고 했는데
associatedtype을 이용해서 제네릭을 이용하는 경우에도 where을 이용한 상세 제약조건 설정이 가능하다.
// Container 프로토콜 정의
protocol Container {
associatedtype Item
var items: [Item] { get }
func contains(item: Item) -> Bool
}
// Item 타입이 Equatable을 준수해야 하는 제약을 추가
extension Container where Item: Equatable {
func contains(item: Item) -> Bool {
return items.contains(item)
}
}
이런 식으로 associatedtype에 where을 이용해서 제약조건을 추가 할 수 있다.
// 제네릭 컨테이너 정의
struct Container<T> {
var items: [T]
// 서브스크립트에 where 절을 사용하여 Item 타입이 Comparable을 준수할 때만 서브스크립트 제공
subscript<U>(index: Int) -> U? where T == U {
return items[index] as? U
}
}
// 사용 예시
let intContainer = Container(items: [1, 2, 3, 4, 5])
if let value: Int = intContainer[2] {
print(value) // 출력: 3
}
let stringContainer = Container(items: ["apple", "banana", "cherry"])
if let value: String = stringContainer[1] {
print(value) // 출력: banana
}
swift에서 제네릭을 사용해서 코드중복을 줄일 수 있다.
함수부터 시작해서 클래스,구조체화 같은 타입 그리고 프로토콜과 서브스크립트 등에서도 제네릭을 이용할 수 있다.
제네릭에는 다양한 제약조건을 설정할 수 있다. :콜론을 이용하거나 where을 이용하면 되는데 where을 이용하면 상세한 제약조건을 설정할 수 있다.