[Swift] 제네릭 (Generic)

제네릭을 사용하면 모든 타입에서 작동할 수 있는 유연하고 재사용가능한 함수타입을 정의할 수 있음

Array, Dictionary 타입은 모두 제네릭한 콜렉션타입이다.

배열은 [Int],[String] 등 타입이 다른 배열을 생성 가능하다. 따라서 배열 또한 제네릭 타입임

제네릭이 해결하는 문제 (The Problem That Generics Solve)

다음 예를 보자

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)

성공적으로 변경 된다.

문제는 여기서 일어난다

만약 변경하려는 타입이 Int가 아닌 Double, String 이라면

같은 함수를 타입만 바꿔 추가로 선언해줘야 한다

func swapTwoDoubles(_ a: inout Double, _ b: inout Double)
func swapTwoStrings(_ a: inout String, _ b: inout String)

이러한 문제를 제네릭을 사용하면 모든 타입의 두 값을 교환할 수 있는 유연한 하나의 함수를 작성할 수 있다.

제네릭 함수 ( Generic Functions)

위의 함수를 제네릭 버전으로 사용해보자

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

함수의 제네릭 버전은 실제 타입의 이름 대신 자리 표시자 이름(여기서는 T를 사용함)을 사용한다

자리표시자 이름은 T가 무엇인지 말해주지 않지만 무엇이든 간에 abT 타입으로 말한다

T에 사용될 실제 타입은 이 함수가 실행될 때 결정된다

자리표시자는 함수의 이름 뒤 꺽쇠<> 를 감싼 부분에 들어가며 이를 Swift에게 알려준다

따라서 Swift는 자리표시자 이름에 사용된 이름의 실제 타입을 찾지 않는다

이 함수는 이제 두 값이 같은 타입인 한 모든 타입의 두 값을 교환할 수 있는 유연한 함수가 된다.

T에 사용할 타입은 함수에 전달된 인수의 값 타입으로 추론하여 결정된다

var a = "aaa"
var b = "bbb"
swapTwoValues(&a, &b)
print("\(a) \(b)")
var doubleA = 1.0
var doubleB = 2.0
swapTwoValues(&doubleA, &doubleB)
print("\(doubleA) \(doubleB)")

인수에 a와 b를 전달 할 때는 TString 타입으로 결정되고

인수 doubleA와 doubleB를 전달할 때는 TDouble 타입으로 결정 된다

타입 파라미터 ( Type Parameter )

자리표시자 T는 타입 파라미터의 예이다

타입 파라미터는 자리표시자 타입을 지정하고 이름을 정한다

함수명 바로 뒤에 꺾쇠 사이에 작성된다 ex) <T>

타입 파라미터를 지정하면 함수 매개변수의 타입을 정의하고

함수의 반환 타입, 함수 내에서 타입으로 사용할 수 있다

func genericMethod<T>(_ a: T, b: T) -> T {
    var sum: T = a + b
    return sum
}

genericMethod 함수는 타입 파라미터 <T>를 정의

genericMethod 함수는 T타입의 매개변수 a, b를 입력 받아 T 타입을 반환 하는 함수

함수 내에서 변수 sumT 로 정의

꺾쇠 괄호 안에 쉼표(, )로 구분하여 둘 이상의 타입 파라미터를 제공할 수 있음

func someGenericMethod<T, V>(_ a: T, b: V)

Generic Types

제네릭 함수 외에 제네릭 타입을 정의할 수 있음

Array, Dictionary와 유사한 방식으로 모든 타입에서 사용할 수 있는

사용자 지정 Class, Struct, Enum을 제네릭 타입으로 사용할 수 있다

아래는 일반적인 Int 타입의 값을 가지는 스택을 구현한 코드이다

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

IntStack은 구조체로서

Int 타입의 값을 가지는 [Int]타입의 프로퍼티 items를 가지고 있고

push, pop 메서드를 이용하여 items의 값을 수정함.

items의 값을 수정해야 하기 때문에 mutating 키워드를 사용

이 코드는 잘동작하지만 Int 타입의 값에서만 동작한다.

이를 보다 유연한 제네릭 코드로 바꿔보자

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

타입 파라미터 <Element>를 정의 하였고

각 프로퍼티, 메서드의 매개변수, 메서드의 반환 타입에 타입 파라미터 Element를 사용하여

유연한 제네릭 타입의 Stack 자료구조가 되었다.

여기서 Element는 다음 위치에서 자리 표시자로 사용이 됨

  1. Element 타입의 비어 있는 배열로 초기화 되는 items 프로퍼티를 생성
  2. push(_:) 메서드에서 Element 타입의 매개변수 item
  3. pop() 메서드에서 반환되는 Element 타입의 값

이 스택은 제네릭 타입이기 때문에 Array, Dictionary 처럼 Swift에서 모든 유효한 타입의

스택을 생성할 수 있음

인스턴스 생성은 items저장할 타입을 꺾쇠 안에 작성하여 생성

var mStack = Stack<Double>() 

mStack.push(5.0)
mStack.push(6.0)
mStack.push(7.0)

print(mStack.pop()

제네릭 타입의 확장 (Extending a Generic Type)

제네릭 타입을 확장할 때는 확장의 정의에 타입 파라미터를 작성하지 않아도 됨

원래 정의부에 있는 타입 파라미터명확장 내에서 그대로 사용이 가능

// 제네릭 타입을 확장할 땐 타입 파라미터를 명시하지 않아도 됨
extension Stack {
    // 확장 안에서 타입 파라미터를 자리표시자로 사용 가능
    var topItem: Element? {
        items.isEmpty ? nil : items[items.count - 1]
    }
}

제네릭 타입의 정의부에서 정의한 타입 파라미터 Element

확장 안에서 자리표시자로 사용할 수 있음

제네릭 타입의 확장에는 추후 설명할 제네릭 Where절과 확장에서 새로운 함수를 사용하기 위해 확장 타입의 인스턴스가 충족해야 하는 요구 사항도 포함할 수 있습니다

타입 제약 조건(Type Constrains)

타입 제약 조건은 타입 파라미터가 다음과 같아야 함을 요구 하는 것

  1. 특정 클래스를 상속
  2. 특정 프로토콜을 준수
  3. 특정 프로토콜 타입

예를 들어 Dictionary타입의 Key로 사용할 수 있는 타입은 Hashable 가능해야 함

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

따라서, Hashable 프로토콜을 준수해야 DictionaryKey로 사용할 수 있음을 제약조건으로 지정

이처럼 사용자 지정 제네릭 타입을 만들때, 고유한 타입 제약 조건을 정의할 수 있으며

이러한 제약 조건은 제네릭 프로그래밍에 많은 기능을 제공한다

모든 Swift의 기본 타입 ( Int, Double, String, Bool 등)은 기본적으로 Hashable 함

타입 제약 조건 구문 (Type Constraint Syntax)

타입 파라미터 뒤에 :(콜론) 으로 구분된 class, protocol과 같은 제한할 타입을 지정하여 정의

제네릭 함수의 제약 조건에 대한 기본 구문은 다음과 같다

(제네릭 타입도 동일하다)

// T는 SomeClass를 상속 받는 타입이어야함
// U는 SomeProtocol을 준수하거나 상속받는 타입이어야 함
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {}

struct SomeStruct<T: SomeStruct, U: SomeProtocol> { }

타입 제약조건의 사용 (Type Constraint in Action)

제네릭 함수가 아닌 일반 함수 findIndex(ofString:in:) 함수를 보자

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

이 함수는 배열 array에서 발견된 첫 일치하는 문자열의 인덱스를 반환하고 찾지 못한다면 nil을 반환함

let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
    print("The index of llama is \(foundIndex)")
}
// The index of llama is 2

이 함수를 제네릭 함수로 변경해보자

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

타입 파라미터 T를 선언하고 각 인자 레이블의 타입을 T, [T]로 정의 하였을 뿐 이전 함수와 똑같음.

실행하면 컴파일 되지 않는다.

Binary operator ‘==’ cannot be applied to two ‘T’ operands

value, valueToFind의 타입은 모두 T이다.

T 타입은 연산자 ==을 사용할 수 없다라는 에러메세지이다.

Swift에서는 모든 타입이 같음 이라는 == 연산자를 사용하여 비교할 수 있지 않다

예를 들어

struct SomeStruct {}
let structType = SomeStruct()
let structType2 = SomeStruct()
if structType == structType2 { ... }

structTypestructType2== 연산자로 비교할 수 있지 않음을 알 수 있다

이 때문에 모든 타입을 대변하는 T== 연산자를 사용할 수 있음을 보장할 수 없으며,

이러한 코드를 컴파일 할 때 컴파일 에러가 발생된다.

그렇다면 == 연산자를 사용하는 타입들은 뭐가 다른걸까?

Swift 표준 라이브러리는 해당하는 타입의 두 값을 비교하기 위해 == 연산자와 != 연산자를 구현하여

비교할 수 있게 하는 Equatable 프로토콜을 가지고 있음.

Equatable 프로토콜을 준수하는 타입들만이 같음 혹은 같지 않음을 비교할 수 있으며

Swift의 표준 타입들은 Equatable 프로토콜을 자동으로 지원한다

findIndex<T>(of:in:) 함수에서 문제가 되는 부분은 T 타입끼리의 비교이다.

따라서 T 타입은 Equatable 프로토콜을 지원하는 타입이라고 제약조건을 명시하면 등호 연산자를 지원하는 타입임을 Swift가 알고 있기 때문에 안전하게 사용이 가능하다

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

<T: Equatable> : Equatable 프로토콜을 준수하는 모든 타입 T

태그:

카테고리:

업데이트:

댓글남기기