공부 연습장 :-)

스위프트_제네릭(Generic in Swift)

|

Swift Generic

스위프트의 제네릭을 이용해서 다양한 타입이 같은 방식으로 동작하는 코드를 작성할 수 있다.
제네릭은 추상화 단계에서, 만약 코드에 동작하는 로직이 똑같은데 적용돼야 할 타입이 여러개면 제네릭을 사용하는 것이 좋다.

Stack을 제네릭으로 만들기

// Element : placeholder type
struct Stack<Element> {
    var items = [Element]()

    mutating func push(_ newItem: Element) {
        items.append(newItem)
    }

    mutating func pop() -> Element? {
        guard !items.isEmpty else {
            return nil
        }
        return items.removeLast()
    }
}

// execute Stack<Int>
var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.pop()) // Optional(2)

  • 여기서 사용된 Element는 placeholder타입으로, 일종의 자리 맡기 타입 이다.
  • 제네릭이 선언된 구조체를 사용할 때는 코드 하단부의 execute 부분처럼, 사용할 타입을 <>안에 기재해준다.
    만약 예시처럼 Stack<Int>로 선언했다면, Stack구조체 안에서 Element로 선언된 모든 부분이 Int로 적용되어서 동작하게된다.

map함수를 제네릭으로 구현하기

  • <T,U>: 자리 맡기용 타입, placeholder
  • _ items: [T]: 입력(파라미터)배열, 배열의 value의 타입은 T
  • _ f: (T) -> (U): 클로저 선언부로, T타입의 인수를 받아 U타입으로 변환한다는 뜻
  • -> [U]: 리턴값은 배열, 그 배열의 value 타입은 U
// f: 부분은 클로저 선언부
func myMap<T,U>(_ items: [T], _ f: (T) -> (U)) -> [U] {
    var result = [U]()
    for item in items {
        result.append(f(item))
    }
    return result
}

// execute myMap()
let letters = ["one","two","three"]
let lengths = myMap(letters) { $0.count }
print(lengths) // [3,3,5]
  • 해당 코드에서 myMap()함수는 String배열을 받아서 각 value의 글자 수를 세어 Int배열을 리턴하는 용도로 사용되었다.
  • myMap함수를 호출할때 괄호 안에 기재된 파라미터 letters는 함수 선언부에서의 _ items: [T]를 의미한다.
  • execute부분에서 볼 수 있듯이, { $0.count }가 바로 위에서 설명된 클로저 선언부이다.
    • String타입인 value각각을 뜻하는 $0.count -리턴값은 Int-를 호출하여 [U]타입 리턴값에 추가한다.

타입 제한 조건

제네릭은 어떤 타입도 올 수 있다는 장점이 있다. 하지만 이렇게 앞으로 사용될 구체적 타입을 컴파일러가 알지 못하는 개방성때문에 오히려 사용에는 제한적인 면이 있다. 이를 보완하기 위해 아래와 같은 두 가지 타입 제한 조건을 둔다.

  1. 타입은 어떤 클래스의 서브 클래스가 되어야 한다.
  2. 타입은 프로토콜을 준수해야한다.

한 가지 예시로, 딕셔너리의 Key값이 되는 타입은 Hashable 프로토콜을 반드시 준수해야하는 조건이 있는데, 이는 물론 제네릭의 타입제한조건때문이다.
이러한 타입제한조건은 제네릭을 사용하는 콜렉션들이 더 견고해지고 정확하게 동작할 수 있게 만든다. 딕셔너리의 Key가 Hashable 프로토콜을 준수해야하는 제한 조건을 둠으로써 딕셔너리의 key값 하나하나는 고유한 값이 되어 value들을 대표할 수 있게 되는것이다.

타입 제한 조건을 적용하여 동일성 판단

  • 프로토콜을 준수하는 타입 T를 사용하여 파라미터의 값이 같은지 판단하는 함수를 만들었다.
  • 프로토콜을 준수하는 placeholder타입은 <T: Equatable>와 같이 선언하면 되고, 상속일때도 마찬가지다.
func checkEqual<T>(_ first: T, _ second: T) -> Bool  {
    return first == second
} // Error: Binary operator '==' cannot be applied to two 'T' operands

func newCheckEqual<T: Equatable>(_ first: T, _ second: T) -> Bool  {
    return first == second
}
  • 이 코드의 위쪽에 선언된 checkEqual() 함수는 에러가 발생한다. T가 어떤 타입이 올지 컴파일러는 예측할 수 없기 때문에 비교연산자가 작동하는지 장담할 수 없어서 에러를 발생시킨다.
  • newCheckEqual()을 보면 placeholder로 T가 선언되긴했지만 그 T는 Equatable을 준수하는 타입만 올 수 있기때문에 컴파일러는 비교연산자가 작동할 것임을 알 수 있다.

타입 제한 조건을 적용한 동일성 판단 두번째

  • 프로토콜을 사용해서 T,U 타입이 달라도 비교가 가능하도록 만들 수 있다.
func checkIfDescriptionMatch<T: CustomStringConvertible, U: CustomStringConvertible>(_ first: T, _ second: U) -> Bool  {
    return first.description == second.description
}

print(checkIfDescriptionMatch(Float(1.0), Double(1.0)))

Comments