[Swift 프로그래밍] 제네릭 (Generic)

이정훈·2023년 2월 1일
0

Swift 기본

목록 보기
21/22
post-thumbnail

본 내용은 스위프트 프로그래밍 3판 (야곰 지음) 교재를 공부한 내용을 바탕으로 작성 하였습니다.

이번 포스트에서는 Swift의 강력한 기능 중 하나인 제네릭(Generic)에 대하여 알아보려고 한다.

먼저 위키백과에 정의 되어 있는 제네릭프로그래밍 정의는 다음과 같다.

제네릭 프로그래밍(영어: generic programming)은 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어 재사용성을 높일 수 있는 프로그래밍 방식이다.

사실 이미 우리는 Array, Dictionary, Set 등을 사용하면서 예를 들면,

//배열 선언
var arr: Array<Int> = Array<Int>()    
//딕셔너리 선언
var dic: Dictionary<String, Int> = Dictionary<String, Int>()

위와 같이 제네릭 기능을 사용하고 있었다.

Array나 Dictionary에 타입을 전달하여 Int 타입의 배열이나 String 타입의 key와 Int 타입의 value를 가지는 Dictionary를 구현하는 등 코드의 재사용성과 코드의 중복을 줄이는 모습을 볼 수 있었다.

만약 collection 타입들이 제네릭 타입이 아니었다면, Int 타입을 요소로 가지는 Array 구조체와 String 타입을 요소로 가지는 Array 등 구조체를 모두 따로따로 구현해 주어야 했을 것이다.

제네릭 표현 방법


제네릭을 사용하고자 할때는 타입 이름 혹은 함수명 뒤에 꺾쇠 괄호(<>)를 사용하여 제네릭을 위한 타입 매개변수를 표시하여 제네릭을 사용할것임을 알린다.

제네릭을 사용할 타입 이름<타입 매개변수>
제네릭을 사용할 함수명<타입 매개변수> (함수 매개변수)

Dictionary 타입과 같이 여러 개의 타입 매개변수를 지정해 주기 위해 다음과 같이 표현 가능하다.

제네릭을 사용할 타입 이름<타입 매개변수1, 타입 매개변수2...>

제네릭 함수


다음은 제네릭을 이용한 함수를 구현한 코드이다.

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

var num1: Int = 3
var num2: Int = 10
swapValues(&num1, &num2)
print(num1, num2)    //10 3

var str1: String = "A"
var str2: String = "B"
swapValues(&str1, &str2)
print(str1, str2)    //B A

위에서 언급 한것과 같이 제네릭을 이용하기 위해 함수명 옆에 꺾쇠괄호(<>)를 사용하여 타입을 전달 받을 매개변수를 선언하였고 이때 사용된 타입 매개변수 T를 placeholder라고 한다.

함수를 선언한 당시 placeholder인 T의 타입을 알 수 없다. 하지만 함수의 매개변수 a와 b가 모드 T 타입인 것을 봐서 두 매개변수 동일한 타입을 전달 받는 것을 알 수 있다.

함수가 호출되었을때 T의 타입이 확정되며, 함수의 매개변수로 전달된 전달인자의 타입이 T의 타입으로 확정이 된다. 예를 들어 함수의 전달인자로 Int 타입이 전달 되었다면, T는 Int 타입이 되고, String 타입이 전달 되었다면, T는 String 타입이 된다. 즉, 함수가 호출할때 마다 다른 타입으로 동작한다.

placeholder 또한 매개변수의 일종으로 함수의 매개변수 타입, 반환 타입, 함수 내부 변수 타입으로 지정할 수 있다.

제네릭 타입


다음은 선입선출(FIFO) 자료구조인 queue를 구현하기 위해 구조체를 선언하고 queue에 들어올 요소의 타입을 제네릭으로 구현한 코드이다.

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

var intQueue: Queue<Int> = Queue<Int>()

intQueue.push(1)
intQueue.push(2)
print(intQueue.items)    //[1, 2]
intQueue.pop()
print(intQueue.items)    //[2]

var strQueue: Queue<String> = Queue<String>()

strQueue.push("A")
strQueue.push("B")
print(strQueue.items)    //["A", "B"]
strQueue.pop()
print(strQueue.items)    //["B"]

var anyQueue: Queue<Any> = Queue<Any>()

anyQueue.push(1)
anyQueue.push("A")
print(anyQueue.items)    //[1, "A"]
anyQueue.pop()
print(anyQueue.items)    //["A"]

위의 코드에서는 placeholder로 Element라는 타입 매개변수를 사용하였다.

제네릭 함수는 함수 전달인자의 타입으로 placeholder의 타입이 알아서 지정이 되었지만 제네릭 타입의 경우 타입 매개변수로 타입을 직접 전달해야 되며 위의 코드와 같이 Queue<String> 처럼 타입을 전달한다.

또한 위에서 설명한것과 같이 Element로 타입을 전달인자로 전달 받아 내부 변수의 타입, 메서드의 반환 값 등으로 사용하는 것을 볼 수 있다.

제네릭 타입 제약


위에서 본 제네릭 함수나 제네릭 타입은 모두 어떠한 제약 사항 없이 모든 타입을 타입 매개변수로 전달할 수 있었다.

하지만 특정 타입을 상속 받은 타입이나, 특정 프로토콜을 준수하는 타입만이 필요하다면?

이럴때 타입 매개변수에 제약 사항을 추가할 수 있다. 여기서 중요한 점은 타입 제약으로 추가할 수 있는 제약 사항으로 class 타입과 protocol로 제한한다.

예를 들어 위의 Queue 구조체에서 Element를 Hashable 프로토콜을 준수하는 타입만을 전달 받고 싶다면 다음과 같이 타입 제약을 구현할 수 있다.

struct Queue<Element: Hashable> {
	...

만약 두개 이상의 제약 사항을 추가하고 싶다면 다음과 같이 where절을 사용하여 나머지 제약 사항을 추가할 수 있다.

struct Queue<Element: Hashable> where Element: BinaryInteger {
	...

protocol 연관 타입


연관 타입(Associated Type)이란 protocol에서 쓸 수 있는 placeholder로 generic에서와 마찬가지로 어떤 타입인지는 모르겠으나, 이러한 타입이 사용될 것이라는 알려주는 것을 protocol에서 사용하고자 할때 사용할 수 있다.

protocol SomeProtocol {
    associatedtype someType    //associated type
    var count: Int { get }
    func push(_ item: someType)
    func pop() -> someType
    subscript(i: Int) -> someType { get }
}

연관 타입을 정의하는 방법은 위와 같이 associatedtype 키워드를 사용하여 정의한다. 위의 예시에서는 someType이라는 placeholder를 사용하였다.

하지만 protocol을 준수하는 구현부에서 연관 타입에 특정 타입을 지정해줄 필요는 없으며 해당 연관 타입을 일관성 있게 하나의 타입으로만 구현해 주면 된다.

class AssociatedType_Queue: SomeProtocol {    //associated type is Integer    
    var items: [Int] = [Int]()
    var count: Int {
        return items.count
    }
    
    func push(_ item: someType) {
        items.append(item)
    }
    
    func pop() -> Int {
        return items.removeFirst()
    }
    
    subscript(i: Int) -> Int {
        return items[i]
    }
}

위의 예시에서는 연관 타입을 모두 Int 타입으로 구현하였다.

만약 이렇게 연관 타입을 구현하는 것이 명확하지 않다고 느껴진다면 typealias 키워드를 사용하여 연관 타입에 타입을 전달해 줄 수도 있다.

class stack: SomeProtocol {
	typealias someType = Int
    ...

Subscript 제네릭


서브스크립트에서 제네릭을 구현하는 방법은 일반적인 타입에 제네릭을 구현하는 것과 유사하다.

extension Generic_Queue {
    subscript<Seq: Sequence>(indices: Seq) -> [Element] where Seq.Iterator.Element == Int {
        //Seq type must conform the Sequence protocol and it's Iterator Element type is a Integer
        var sliced: [Element] = [Element]()
        for index in indices {
            sliced.append(self[index])
        }
        
        return sliced
    }
}

placeholder로 Seq를 사용하였으며, Seq는 Sequence protocol를 준수하고 Iterator의 Element의 타입이 Int 타입인 제약 사항을 충족하는 타입의 전달인자에 따라 유동적으로 동작할 수 있다.

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글