본문 바로가기
프로그래밍/Swift

Swift 제네릭 함수, 제네릭 타입

by Mr-후 2021. 7. 15.
반응형

제네릭은 중복을 피하면서 매우 유연하고 재사용이 가능한 코드를 작성할 수 있게 해 준다. 

스위프트 언어에서 제네릭은 옵셔널의 예로 사용 된다. 옵셔널 타입은 두 개의 사용 가능한 값인 None과 Som(T)로 이뤄진 열거형으로 정의돼 있으며, 여기서 T는 적절한 타입의 연관 타입을 나타낸다. 옵셔널에 nil을 대입하면 None 값을 갖게 되고, 옵셔널에 어떠한 값을 대입하면 적절한 타입의 연관 값을 갖는 Some 값을 갖게 될 것이다. 

enum Optional<T> {
	case None
	case Some(T)
}

T는 옵셔널과 연관된 타입, 플레이스홀더인 T는 제네릭을 정의하는데 사용한다. 

제네릭을 사용해 중복을 피하는 예는 다음과 같다. 


두 변수의 값을 교환하는 함수를 만들고자 할 때 여러 타입(Integer, Double, String)별로 지원해야 할 경우 각각의 함수를 작성해야 한다. 

func swapInts(a: inout Int, b: inout Int) {
    let tmp = a
    a = b
    b = tmp
 }
 
 func swapDoubles(a: inout Double, b: inout Double) {
     let tmp = a
     a = b
     b = tmp
 }
 
 
 func swapStrings(a: inout String, b: inout String { 
     let tmp = a
     a = b 
     b = tmp
 }

이처럼 여러 타입을 교환해야 하는 요구사항이 생길 때마다 함수를 추가해줘야 하는데 제네릭을 사용할 경우 모든 중복 코드를 제거한 훨씬 더 멋지고 간단한 해결 방법을 제시할 수 있다.

 

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

 

대문자 T에는 특별한 게 없으며 T 대신에 다른 어떤 유효한 식별자를 사용할 수도 있다. 

대부분의 문서에서는 제네릭 플레이스홀더를 T(타입:Type) 또는  E(요소:Element)로 정의한다. 

제네릭 함수를 호출 하는 예는 다음과 같다. 

var a = 5
var b = 10

swapGeneric(a: &a, b: &b) 
print("a: \(a), b: \(b)")

이제 다른 타입을 교환해는 예제다. 

var c = "my string 1"
var d = "my string 2" 

swapGeneric(a: &c, b: &d)
print("c:\(c), d:\(d)")

 

서로 다른 값을 교환하려고 할 때는 오류가 발생한다. 

cannot convert value of type Stirng to expected argument type Int 

var a = 5
var c = "my string 1"
swapGeneric(a: &a, b: &c)

 

여러 개의 제네릭 타입을 사용해야 하는 경우에는 쉼표를 사용해 플레이스홀더를 나누면 플레이스홀더를 여러개 생성할 수 있다. 

func testGeneric<T,E>(a: T, b:E) {
    print("\(a) , \(b)")
}

이렇게 정의하면 플레이스홀더 T에 타입을 하나 설정하고 플레이스홀더E에 다른 타입을 설정할 수 있다. 

func genericEqual<T>(a: T, b: T) -> Bool {
    return a == b
}

//발생 에러 : binary operator '==' cannot be applied to Two 'T' operands

코드가 컴파일되는 시점에서는 인자의 타입을 모르기 때문에 스위프트는 타입에 동등 연산자를 사용할 수 있는지를 알지 못하며, 이와 같은 이유로 인해 에러가 발생하게 된다. 

이 경우 스위프트에게 해당 타입이 어떠한 기능을 갖고 있을 것이라는 점을 알려 줄 방법이 있는데 바로 "타입제약(type constraints)" 이 이러한 일을 처리한다. 

타입제약 : 제네릭 타입은 반드시 구체적인 클래스를 상속하거나 특정 프로토톨을 따라야 한다. 

타입제약은 제네릭 타입에서 부모 클래스나 프로토콜에 정의된 메소드나 프로퍼티를 사용할 수 있게 해준다. 이번에는 Comparable프로토콜을 사용하기 위해 genericEqual()함수를 다시 작성 해본다. 

func testGenericComparable<T: Comparable>(a: T, b: T) -> Bool {
    return a == b 
}

 

여러 제네릭 타입을 선언했던 것처럼 타입 제약도 여러 개 선언할 수 있다. 

func testFunction<T: MyClass, E: MyProtoco>(a: T, b: E) {
   //
}

이 함수에서 플레이스홀더 T로 정의된 타입은 반드시 MyClass클래스를 상속해야만하고, 플레이스홀더 E로 정의된 타입은 반드시 MyProtocol 프로토콜을 구현해야만 한다. 

 

제네릭 타입 

스위프트의 배열과 옵셔널이 어떠한 타입과도 함께 동작할 수 있는 것처럼 제네릭 타입은 어떠한 타입과도 동작이 가능한 클래스나 구조체 또는 열거형을 의미한다. 제네릭 타입의 인스턴스를 생성할 때에는 인스턴스가 동작할 타입을 명시한다. 타입이 한번 정의되면 해당 인스턴스 동안에는 타입을 변경할 수 없다. 

만들어볼 제네릭 타입은 간단한 List구조체이다. 

struct List<T> {
    var items = [T]()
    
    mutating func add(item: T) {
        items.append(item)
    }
    
    func getItemAtIndex(index: Int) -> T? {
        if items.count > index {
            return items[index]
        } else {
            return nil
        }
    }
}


//사용 예 

var list = List<String>
list.add(item: "Hello")
list.add(item: "World")
print(list.getItemAtIndex(index: 1))

list라 불리는 List타입의 인스턴스를 생성하는 것으로 시작, 저장할 타입으로 String타입을 정의한다. 

제네릭 메소드에서 여러 플레이스홀더를 사용했던 것과 유사하게 여러 플레이스 홀더 타입에서도 제네릭 타입을 정의할 수 있다. 

class MyClass<T, E> {
    //
}


var mc = MyClass<String, Int>() 

//다음 코드는 Comparable프로토콜을 따르는 제네릭 타입임을 보장하기 위해 타입 제약을 사용하는 방법이다. 

struct MyStruct<T: Comarable>{}

 

반응형