제네릭

피터·2022년 9월 17일
0
post-thumbnail

제네릭을 사용하면 어떤 장점이 있을까요?

  • 어떤 타입에도 유연하게 대응할 수 있습니다.
  • 제네릭으로 구현한 기능과 타입은 재사용하기 좋습니다.
  • 코드의 중복을 줄일 수 있게 되어 깔끔하고 추상적인 표현이 가능합니다.

제네릭을 사용하기 위해서는 제네릭이 필요한 타입 또는 메서드의 이름 뒤에 홀화살괄호 기호(<>) 사이에 제네릭을 위한 타입 매개변수를 써주어 제네릭을 사용할 것임을 표시합니다.

struct 제네릭을사용할타입<T> {
    var 이건제네릭프로퍼티: T
}

func 제네릭을사용할메서드<Hero>(a: Hero) { }
  • 제네릭 사용 예시
    func printArray(array: [Int]) {
        for element in array {
            print(element)
        }
    }
    
    func printArray(array: [String]) {
        for element in array {
            print(element)
        }
    }
    
    func printArray(array: [Double]) {
        for element in array {
            print(element)
        }
    }
    
    func printArray(array: [Float]) {
        for element in array {
            print(element)
        }
    }
    ???? 같은 기능을하는데 오버로딩 함수가 너무 많죠? 이 경우 제네릭을 사용하면 간결해지고 재사용성이 높아집니다.
    func printArray<G>(array: [G]) {
        for element in array {
            print(element)
        }
    }

제네릭 함수

제네릭 함수에서 실제 타입 이름(Int, String 등)을 써주는 대신에 홀화살괄호 기호 내부에 있는 플레이스홀더를 사용합니다. 플레이스홀더는 타입의 종류를 알려주지 않지만 말 그대로 어떤 타입이라는 것은 알려줍니다.

아래 코드를 한번 봅시다.

func swapToValue<T>(a: inout T, b: inout T) {
    let temp = a
    a = b
    b = temp
}

위 코드의 플레이스홀더 타입 T인 두 매개변수가 있으므로, 두 매개변수가 같은 타입이라는 정도는 알 수 있습니다. T의 실제 타입은 제네릭 함수가 호출될 때 결정됩니다.

그럼 다음 함수는 어떨까요?

func printTwoValue<G, T>(a: G, b: T) {
    print(a)
    print(b)
}

두 플레이스홀더 타입 G와 T가 있고 둘의 타입은 일단 다르지만 사용자에 따라 같게할 수도 있긴 합니다.

반환값으로도 제네릭 타입을 사용할 수도 있습니다.

func printTwoValue<G, T>(a: G, b: T) -> G {
    print(b)
    return a
}

제네릭 타입

제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있습니다.

struct Stack<Element> {
    var array: [Element] = [Element]()
    
    mutating func push(_ e: Element) {
        array.append(e)
        print(array)
    }
    
    mutating func pop() -> Element {
        defer {
            print(array)
        }
        return array.removeLast()
    }
}

var intStack = Stack<Int>()
intStack.push(4)
intStack.push(5)
intStack.push(100)
intStack.pop()
intStack.push(49)
intStack.pop()
intStack.pop()
intStack.pop()

// [4]
// [4, 5]
// [4, 5, 100]
// [4, 5]
// [4, 5, 49]
// [4, 5]
// [4]
// []

제네릭 타입 확장

만약 익스텐션을 통해 제네릭을 사용하는 타입에 기능을 추가하고자 한다면 익스텐션 정의에 타입 매개변수를 명시하지 않아야 합니다. 대신 원래의 제네릭 정의에 명시한 타입 매개변수를 익스텐션에서 사용할 수 있습니다.

다시 말해, 제네릭 타입의 확장에서 특정 타입일 경우에 동작하는 기능을 추가하고 싶다고 해서 다음과 같이

extension Stack<Int> { }

특정 타입을 명시하면 안된다는 의미입니다. 대신해서 where를 사용해서 할 수 있습니다. 이것은 제네릭 타입의 제약에서 더 다루겠습니다.

extension Stack where Element == Int { }

타입 제약

제네릭 타입은 현재까지 타입에 대한 제약 없이 사용할 수 있었죠?

그럼 만약 특정 타입에 한정되어야만 기능을 처리할 수 있다던가, 제네릭 타입을 특정 프로토콜을 따르는 타입만 사용할 수 있도록 제약을 두어야하는 상황에서는 어떻게 해야 할까요?

이때 타입 제약을 걸어둘 수 있습니다~!

타입 제약은 타입 매개변수가 가져야 할 계약사항을 지정할 수 있는 방법입니다.

단, 타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있습니다!

열거형, 구조체 등의 타입은 타입 제약의 타입으로 사용할 수 없습니다.

일단 코드로 확인해볼까요?

자주 사용하는 Dictionary의 타입 정의를 확인해봅시다.

@frozen public struct Dictionary<Key, Value> where Key : Hashable { }

Dictionary의 키는 Hashable 프로토콜을 준수하는 타입만 사용할 수 있습니다.

이제 적용해봅시다.

위에서 만들었던 Stack 구조체의 제네릭 타입 매개변수에 정수만 오도록 타입 제약을 걸고 싶습니다. 그러면 제네릭 타입 뒤에 콜론 그리고 해당 프로토콜 또는 클래스를 명시해주면 됩니다.

struct Stack<T: Int> {
    var items: [T] = []
    
    mutating func push(_ newValue: T) {
        items.append(newValue)
        
        print(items)
    }
    
    mutating func pop() {
        items.removeLast()
        
        print(items)
    }
}

위 코드처럼 하고 싶지만!!! Int는 구조체라서 타입 제약을 걸 수 없습니다. 대신 정수 타입임을 나타내는 프로토콜을 준수하면 되죠. 이건 구글링으로 .. 찾아보시면

Numeric Protocols라는 것이 있네요! 여기서 BinaryInteger 프로토콜이라는 사실을 확인했고 적용해보면 에러 메세지 없이 타입제약이 가능합니다.

struct Stack<T: BinaryInteger> {
    var items: [T] = []
    
    mutating func push(_ newValue: T) {
        items.append(newValue)
        
        print(items)
    }
    
    mutating func pop() {
        items.removeLast()
        
        print(items)
    }
}

그런데 나는 다른 프로토콜을 준수하는 타입이면 좋겠다! 라고 한다면 where절을 이용해서 추가할 수 있습니다. 추가적인 내용은 where절에서 더 다루도록 하겠습니다.

protocol SomeProtocol { }

struct Stack<T: BinaryInteger> where T: SomeProtocol {
    var items: [T] = []
    
    mutating func push(_ newValue: T) {
        items.append(newValue)
        
        print(items)
    }
    
    mutating func pop() {
        items.removeLast()
        
        print(items)
    }
}

프로토콜의 연관 타입

프로토콜을 정의할 때 연관 타입(Associated Type)을 함께 정의하면 유용할 때가 있습니다.

연관 타입은 프로토콜에서 사용할 수 있는 플레이스홀더 이름입니다. 즉, 제네릭에서 어떤 타입이 들어올지 모를 때, 타입 매개변수를 통해 ‘종류는 알 수 없지만, 어떤 타입이 여기에 쓰일 것이다.’라고 표현해주었다면 연관 타입은 타입 매개변수와 그 역할을 프로토콜에서 수행할 수 있도록 만들어진 기능입니다.

protocol Container {
    associatedtype Item
    
    mutating func append(_ item: Item)
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

Container는 존재하지 않는 타입인 Item을 연관 타입으로 정의하여 프로토콜 정의에서 타입 이름을 활용합니다.

이는 제네릭의 타입 매개변수와 유사한 기능으로, 프로토콜 정의 내부에서 사용할 타입이 ‘어떤 것이어도 상관없지만, 하나의 타입임은 분명하다'라는 의미입니다.

이제 Container프로토콜을 준수하는 하나의 타입을 만들어볼까요?

class MyContainer: Container {
    var items: [String] = []
    func append(_ item: String) {
        
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> String {
        return items[i]
    }
}

연관 타입인 Item 대신 실제 타입인 String 타입으로 구현해주었고, 해당 프로토콜의 요구사항을 모두 충족하므로 큰 문제가 없습니다. append에서 item의 타입이 String이라고 명시해주어서 컴파일러가 자동으로 연관 타입에 대해서 알게된 것입니다.

아니면 더 확실하게 하기 위해서

class MyContainer: Container {
    typealias Item = String
    var items: [Item] = []
    
    func append(_ item: String) {
        items.append(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> String {
        return items[i]
    }
}

위 처럼 연관 타입인 Item을 무엇으로 쓸 것이다! 라고 명시해주는 방법도 있습니다.

제네릭 서브스크립트

struct Stack<Element>: Container {
    typealias Item = Element
    
    var items: [Element] = []
    
    mutating func push(_ item: Element) {
        self.items.append(item)
    }
    
    mutating func pop() {
        self.items.removeLast()
    }
    
    mutating func append(_ item: Element) {
        self.push(item)
    }
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Element {
        return items[i]
    }
}

extension Stack {
    subscript<Indices: Sequence>(indices: Indices) -> [Element] where Indices.Iterator.Element == Int {
        var result = [Item]()
        for index in indices {
            result.append(self[index])
        }
        return result
    }
}

var integerStack = Stack<Int>()
integerStack.append(1)
integerStack.append(3)
integerStack.push(4)
integerStack.push(5)
integerStack.push(10)

print(integerStack[0...2])

// [1, 3, 4]

서브스크립트도 제네릭을 활용하여 타입에 큰 제한 없이 유연하게 구현할 수 있습니다. 물론 타입 제약을 사용해서 제네릭을 활용하는 타입에 제약을 줄 수도 있습니다.

자료 출처: 야곰 스위프트 프로그래밍 3판

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
profile
iOS 개발자입니다.

0개의 댓글