제네릭(Generic)

정의

코드의 유연성 및 재사용성을 높이기 위한 추상화 기법. 코드 중복 방지, 의도를 명시적으로 드러낼 수 있다.

사용용도

  • 표준 라이브러리를 만드는 핵심 기법이다.
  • Array, Dictionary 타입이 generic collection.
  • Array : Int, String, ... 등등 타입을 사용 가능

제네릭 함수

  • 2개 정수를 입력 받아 서로 값을 바꾸는 예제
func swapInts(inout a: Int, inout _ b: Int) {
    let tempA = a
    a = b
    b = tempA
}

var firstInt = 2
var secondInt = 3

swapInts(&firstInt, &secondInt)
  • 2개 문자열을 입력 받아 서로 값을 바꾸는 예제
func swapStrings(inout a: String, inout _ b: String) {
    let tempA = a
    a = b
    b = tempA
}
  • 타입에 상관없이 동일 타입의 2개 값을 받아서 서로 값을 바꾸는 예제
func swapValues<T>(inout a: T, inout _ b: T) {
    var tempA = a
    a = b
    b = tempA
}

예제

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

타입 인자(Type parameter)

  • 위에 예제에서 T를 타입 인자라고 부른다.
  • 함수 바로 뒤에 <T>와 같은 형태로 사용한다.

타입 인자 이름 붙이기

  • Dictionary
    • 에서 Key, Value와 같이 타입 인자에 속성을 설명하는 이름을 붙인다.
    • 타입 인자와 제네릭 타입 사이의 관계를 코드를 읽는 사람 이해를 돕는다.
    • 전통적으로 T, U, V와 같이 단일 대문자로 표현한다.
    • 주의 : 타입이라는 것을 표시하기 위해서 대문자를 사용한다.

제네릭 타입

  • 제네릭 함수를 위해서 봤고 이번에는 개발자가 직접정의할 수 있는 제네릭 타입에 대해서 알아보자.
  • class, struct, enum와 함께 사용된다. (Array, Dictionary)
  • Stack이라는 제네릭 콜렉션 타입 예제 Stack
    1. 현재 Stack에 3개 값이 있다.
    2. 4번째 값을 추가해서 맨 위에 위치하게 된다.
    3. 이제 Stack은 총 4개 값을 가지고 있다.
    4. pop을 호출해서 Stack 맨 위에 있는 값을 빼낸다.
    5. pop을 하고 난 후에 1에서와 같이 3개 값을 가지고 있는 상태가 된다.
  • 우선 제네릭이 아닌 Int 값에 대한 Stack을 구현해 보자.
struct IntStack {
    var items = [Int]()
    mutating func push(item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}
  • items는 Array 프로퍼티로 Int 값을 저장하는 역할을 수행한다.
  • push와 pop 메소드를 제공한다.
  • 돌발 퀴즈 : 왜 mutating로 지정하였을까?
  • 답 : 메소드가 프로퍼티의 값을 변경하는 경우 mutating 키워드를 붙여줘야 한다.
  • 자 이제 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("cat")
stackOfStrings.push("dog")
stackOfStrings.push("cow")

Stack2

let fromTheTop = stackOfStrings.pop()

Stack3

  • 코드 읽기
    1. items 프로퍼티는 Element 타입의 값을 가지는 Array 값이다.
    2. push 메소드는 Element 타입의 item을 배열에 넣는다.
    3. pop 메소드는 Element 타입의 값을 반환한다.

제네릭 타입 확장

  • 기존 제네릭 타입을 확장하기
    • 아래 예제와 같이 extension을 붙인다.
    • 기존 제네릭 타입의 타입 인자를 그대로 사용할 수 있다.
  • 읽기 전용 프로퍼티인 topItem
  • pop하지 않고 최고 위에 있는 값을 반환한다.
    extension Stack {
     var topItem: Element? {
         return items.isEmpty ? nil : items[items.count-1]
     }
    }
    
  • Quiz : ?를 사용한 이유는?
  • 답 : nil이 될 수 있는 경우 사용. isEmpty에 따라 nil을 반환할 수 있기 때문이다.
if let topItem = stackOfStrings.topItem {
    print("The top item on the stack is \(topItem).")
}

타입 제약

  • 이전 제네릭 swap 함수를 봤다. 어떤 값이든 올 수가 있었다. 하지만 특정 타입에 제약을 걸고 싶다면?
  • Dictionary 타입의 경우 key에 제한이 걸려있다. 이 제약은 hashable해야한다는 것이다. key의 속성은 유일무이해야 하며 포함하고 있는지 여부를 바로 확인할 수 있어야 한다.
  • 참고로 Swift의 기본 타입들 모두 hashable하다.(String, Int, Double, Bool)

문법

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

}
  • 2개 타입 인자를 가진다. 첫번째는 T이며 SomeClass의 서브클래스라는 제약을 가지고 있다. 두번째는 U이며 SomeProtocol을 만족시켜야 한다.

타입 제약 동작

  • 일반 함수 findStringIndex 함수가 아래와 같다.
func findStringIndex(array: [String], _ valueToFind: String) -> Int? {
    for(index, value) in array.enumerate() {
        if value == valueToFind {
            return index
        }
    }
    return nil 
}

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
    print("The index of llama is \(foundIndex)")
}
  • 제네릭 함수 findIndex를 만들어 보자.
func findIndex<T>(array:[T], _ valueToFind: T) -> Int? {
    for (index, value) in array.enumerate() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}
  • 위 코드는 컴파일 에러가 발생한다?
  • Quiz : 왜 컴파일 에러가 발생하는가?
  • 답 : if value == valueToFind 부분에서
  • Equatable 성질을 가지도록
func findIndex<T: Equatable>(array: [T], _ valueToFind: T) -> Int? {
    for(index, value) in array.enumerate() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)

let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
  • 타입 T는 Equatable 프로토콜을 만족시켜야 한다는 제약을 가지도록 했다. Equatable 속성을 가지는 Double과 String이 가능하다.

Quiz

  • 정의에 대한 질문
  • 용도에 대한 질문
  • 예제 관련