티스토리 뷰

제네릭은 Java에서 사용하는 제네릭의 개념과 매우 유사한 내용의 개념이다.

 

제네릭을 사용하면 좀 더 유연하고 재사용 가능한 함수와 타입의 코드를 작성하는 것을 가능하게 해준다.


1. 제네릭 함수

우선 제네릭 함수는 아래와 같이 <> 를 사용한다. 아래는 inout 인자를 받아 참조되는 변수의 값을 바꾸는 swap 함수를 제네릭을 사용하여 구현한 것이다.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swift는 실제 실행하는 타입 T가 어떤 타입인지 보지 않는다. swapTwoValues 함수가 실행되면 T에 해당하는 값을 함수에 넘긴다.

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

 

- 타입 파라미터

위에서 사용한 플레이스 홀더 T는 타입 파라미터의 예시이다. 타입파라미터는 보통의 경우 T, U, V와 같은 단일 문자로 이름을 짓거나 Dictionary의 key, value와 같이 엘리먼트 간에 서로 상관 관계가 있는 경우에는 의마가 부여되는 이름으로 선언한다.


2. 제네릭 타입

제네릭 함수에 추가로 제네릭으로 구성된 타입 또한 정의가 가능하다. 

 

아래 예제와 함께 Stack 이라는 제네릭 콜렉션 타입을 어떻게 구현하는 지 확인해보자.

 

우선 Int값을 스택에 넣고, 빼는 함수 IntStack의 구현은 다음과 같다.

struct IntStack {
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

 

이를 제네릭 형태로 구현하면 다음과 같다.

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

 

위 예제와 같이 제네릭을 사용하여 타입 객체 또한 유연하게 사용이 가능하다.


3. 제네릭 타입의 확장

다른 타입 객체와 동일하게 제네릭 타입 또한 익스텐션을 이용하여 확장이 가능하다.

extension Stack {
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1]
    }
}
if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}
// Prints "The top item on the stack is tres."

4. 타입 제한

Swift의 Dictionary 타입은 키 값을 사용한다. 이 때 key는 유일한 값이여야 하기 때문에 hashable 이라는 프로토콜을 반드시 따라야 한다. 

 

제네릭 함수를 선언할 때, 파라미터 뒤에 상속 받아야 하는 클래스를 선언하거나, 반드시 따라야 하는 프로토콜을 명시할 수 있다.

func someFunction<T: SomeClass, U: SomeProtocol> (someT: T, someU: U) {

}

 

- 타입 제한의 실 사용

다음과 같이 한 배열에서 특정 문자를 검색하는 findIndex 함수를 선언한다.

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
    for(index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]

if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama  is \(foundIndex)")
}

위 함수를 실행하면 strings 배열에서 찾기 원하는 문자열 llama 의 인덱스 위치를 찾는 것이 가능하다.

 

이를 제네릭으로 구현하면 다음과 같이 구현할 수  있다.

func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

위 코드는 에러가 발생한다. 이유는 value == valueToFind 의 코드에서 두 값을 비교하게 되는데 두 값을 비교(== 메서드 사용) 하는 경우에는 두 값 혹은 객체가 반드시 Equatable 프로토콜을 따라야 하기 때문이다.

 

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2

5. 연관 타입

연관 타입은 프로토콜의 일부분으로 타입에 플레이스홀더 이름을 부여한다. 다시 말해 특정 타입을 동적으로 지정하여 사용할 수 있다는 의미이다.

 

아래와 같이 Item에 associatedtype을 사용할 수 있다. 이렇게 지정하면 Item은 어떤 타입도 될 수 있다.

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

 

아래 코드에서는 Item을 Int형으로 선언하여 사용한다.

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
    // conformance to the Container protocol
    typealias Item = Int
    mutating func append(_ item: Int) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Int {
        return items[i]
    }
}

 

혹은 아래와 같이 제네릭 타입으로 다시 받아서 사용해도 된다.

struct Stack<Element>: Container {
    // original Stack<Element> implementation
    var items = [Element]()
    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
    // conformance to the Container protocol
    mutating func append(_ item: Element) {
        self.push(item)
    }
    var count: Int {
        return items.count
    }
    subscript(i: Int) -> Element {
        return items[i]
    }
}

 

- 존재하는 타입에 연관 타입을 확장

아래와 같이 기존의 타입 Array에 특정 연관 타입을 추가할 수 있다.

extension Array: Container {}

단, 이는 Array 타입이 Container의 모든 프로퍼티를 동일하게 가지고 있기 때문에 가능한 것이다.


6. 제네릭의 where 절 

제네릭에서도 where절을 사용할 수 있다. 

 

아래 예제는 Container C1, C2 를 비교하며 모든 값이 같을 때 true를 반환하는 allItemsMatch 함수를 구현한 것이다.

func allItemsMatch<C1: Container, C2: Container>
(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.Item == C2.Item, C1.Item: Equatable {
    if someContainer.count != anotherContainer.count {
        return false
    }
    
    for i in 0 ..< someContainer.count {
        if someContainer[i] != anotherContainer[i] {
            return false
        }
    }
    
    return true
}

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
    print("All items match.")
} else {
    print("Not all items match.")
}
// Prints "All items match."

Container의 타입은 다르지만 안에 내용이 같기 때문에 모든 아이템이 일치한다는 결과가 반환된다.

 

- Where 절을 포함하는 제네릭의 익스텐션

Stack의 익셔텐션을 선언하면서 아래와 같이 where절을 포함시킬 수 있다. 다음은 isTop 함수를 익스텐션으로 추가한 코드인데, 이 함수가 추가되는 Stack은 반드시 Equtable 프로토콜을 따라야 한다고 제한을 부여한 코드이다.

extension Stack where Element: Equatable {
    func isTop(_ item: Element) -> Bool {
        guard let topItem = items.last else {
            return false
        }
        return topItem == item
    }
}

아래와 같이 Equatable을 따르지 않는 Stack 에서는 익스텐션에 선언된 함수 isTop을 실행하면 에러가 발생한다.

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

7. 제네릭 서브스크립트

제네릭의 서브스크립트에도 조건을 걸 수 있다.

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