Swift - 제네릭


Swift - Generic


Generic은 Swift의 꽃이라고 한다. 굉장히 많은 곳에서 이용된다고 하는데, 아직 Swift로 다양한 상황을 구현해보지 않아서 와닿지는 않는다. 그래도 많은 곳에서 사용되고 중요한 만큼 정리해두려고 한다.

Generics

제네릭은 모든 타입 혹은 내가 정의한 그 어떤 것과도 같이 동작할 수 있는 유연하고 재사용이 가능한 함수와 타입을 작성할 수 있게 해준다. 따라서 중복되는 코드를 작성하는 것을 피할 수 있고, 명확하고 요약된 형태로 의도를 표현할 수 있다.

제네릭은 Swift의 가장 강력한 특징이고, 거의 모든 Swift 표준 라이브러리는 제네릭으로 작성되어 있다. 나도 모르는 새에 제네릭을 사실 사용하고 있던 것이다. 예를 들어, Swift의 array와 dictionary 타입은 모두 제네릭 컬렉션이다. int 값을 가질 수도 있고, String 값을 가질 수도, 혹은 다른 타입을 가질 수 있는 배열을 만들 수 있다. 비슷하게, 어떤 타입의 값이든 저장할 수 있는 dictionary를 만들 수 있고, 그 타입에는 제한이 없다.

제네릭이 해결하는 문제점들

아래와 같이 두 정수 값을 바꾸는 제네릭하지 않은 함수가 있다.

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
  let tmp = a
  a = b
  b = tmp
}

위 함수는 in-out 파라미터를 이용해서 a, b의 값을 바꾸고 있다. 하지만 이 함수를 Double이나 String 타입의 값에도 적용하려면 더 많은 함수를 아래와 같이 작성해야 한다.

func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

위의 함수들을 보면 내부는 모두 동일하다. 유일한 차이점은 받는 값의 타입이 다르다는 것이다. (Int, String, Double)

함 함수가 어떤 타입이든지 받을 수 있게 작성하면 더 유용하고 간단할 것이다. 제네릭은 이런 함수를 작성할 수 있게 해준다.

제네릭 함수

Generic functions들은 어떤 타입든지 상관없이 작동할 수 있게 해준다. 아래는 위의 함수를 제네릭하게 작성한 것이다.

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

위 제네릭 함수의 내부도 위의 함수들과 동일하다. 다만 다른 점을 아래와 같이 비교할 수 있다.

func swapTwoInts(_ a: inout Int, _ b: inout Int)
func swapTwoValues<T>(_ a: inout T, _ b: inout T)

제네릭 함수는 placeholder를 실제 타입 이름(Int, String, Double)대신 사용한다(여기서는 T). placeholder 타입은 T가 무엇인지 딱히 정의하고 있지는 않지만, 위에서는 a와 b가 모두 T로 같은 타입임을 명시하고 있다. 실제로 T 대신에 들어갈 타입은 위의 함수가 호출되는 순간마다 정해진다.

제네릭 함수와 그렇지 않은 함수의 또 다른 차이점은 바로 함수 이름 옆에 <>사이에 플레이스홀더 이름이 들어간 것이다. 이 꺾쇠는 T가 함수의 플레이스 홀더 타입 이름이라는 것을 알려준다. T가 플레이스 홀더이기 때문에, Swift는 실제 T라는 타입을 찾지 않는다.

Type Parameter

위에서 플레이스 홀더 타입 T는 type parameter의 한 예다. Type parameter는 placeholder type을 정의하고 이름 붙이고, 함수 이름 바로 뒤에 괄호 사이에 위치한다.

Type parameter를 정의하면, 함수의 파라미터의 타입, 함수의 리턴 타입, 함수 내부에서 타입 어노테이션으로 사용할 수 있다. 각 케이스에서 타입 파라미터는 함수가 호출되면 실제 타입으로 교체된다. 여러개의 타입 파라미터 이름을 괄호 안에 쉼표로 구분해서 제공할 수 있다.

Type Parameter 이름 짓기

대부분의 경우에, 파입 파라미터는 dictionary<Key, Value>에서는 Key와 Value, Array에서는 Element라는 이름을 가지고 있다. 이런 이름은 타입 파라미터와 제네릭 타입이나 함수의 관계를 사용하는 사람에게 알려준다. 하지만, 명확환 관게가 없다면, T, U, V와 같은 단일 문자로 이름 짓는 것이 일반적이다.

Associated Types

프로토콜을 정의할 때, 프로토콜 정의에서 하나 이상의 associated type을 정의하는 것이 유용할 때가 있다. associated type은 프로토콜에서 사용되는 타입에 placeholder 이름을 준다. associated type에 사용되는 실제 타입은 프로토콜이 채택될때까지 정의되지 않는다. Associated type은 associatedtype 키워드로 정의된다.

Associated Type의 응용

아래와 같이 Item으로 associated type을 정의한 Conatiner라는 프로토콜이 있다.

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

이 프로토콜은 세 개의 기능을 꼭 충족해야 한다고 정의한다.

  • append 메서드로 새로운 아이템을 추가할 수 있어야 한다.
  • 컨테이너에 있는 아이템의 개수를 셀 수 있어야 한다.
  • Int index 값으로 컨테이너에 있는 아이템을 가져올 수 있어야 한다.

아래는 위의 프로토콜을 제네릭 타입으로 채택한 것을 보여준다.

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

Associated Type을 정의하기 위해 존재하는 타입을 확장하기

이미 존재하는 타입을 프로토콜을 이용해서 확장할 수도 있다. 이 프로토콜은 associated type을 가진 프로토콜도 포함한다.

이를 비어있는 extension으로 정의해도 된다.

extension Array: Container {}

Associated Type에 제약 추가하기

아래와 같이 associated type이 Equatable이라는 프로토콜을 따르도록 제약을 추가할 수 있다.

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