[swift] generic 제네릭

이은수, Lee EunSoo·2024년 10월 7일
0

Swift Basic

목록 보기
20/24
post-thumbnail

개요

제네릭은 클래스나 함수 같은 타입에서 특정 타입에 구애받지 않고 유연하게 설계를 도와주는 기능이다.

Swift에서의 제네릭은 AnyAnyObject와 비슷하게 다양한 타입을 처리할 수 있지만, 타입 안전성을 유지하는 점이 다르다.

Any는 모든 타입을 허용하지만, 타입 안전성이 떨어질 수 있고, AnyObject는 클래스의 인스턴스만 허용한다.

하지만 제네릭을 사용하면 모든 타입을 허용하면서도, 필요에 따라 특정 타입이나 프로토콜을 준수하는 타입만을 사용할 수 있도록 제약을 추가할 수 있습니다.

즉, 제네릭은 특정 타입에 얽매이지 않는 유연함을 제공하면서도, 상황에 맞게 타입 제약을 통해 더 안전하고 구체적인 설계를 할 수 있습니다.

설명

클래스나 구조체같은 타입이나 프로토콜, 함수에서도 사용이 가능하다.

기본적으로 <>각괄호안에 임의 이름을 넣어서 사용한다.
이 이름을 타입 파라메터라고 한다.

class someClass<T>{
	init(){ 
    	//(대충 초기화 하는 내용)
    }
    //(대충 클래스 내부 프로퍼티)
    
}

다음과 같은 형태로 사용한다.

사용하는 종류별 상세 구현형태는 아래에서 자세히 다루겠다.

타입 파라메터

위 코드에서 처럼 <>각 괄호 안에 들어가는 이름을 타입 파라메터라고 하는데 이름을 짓는데도 일종의 국룰이 있다.

제네릭에는 보통 타입에 관한 정보를 나타내기 때문에
다음과 같이 사용하는 정보를 의미하는 단어의 제일 앞 알파벳 1자를 대문자로 적어서 표기한다.

<T> <U> <V>

하지만 swift에서 Element나 Key, Value같은 의미가 있는 관계의 경우 단어 자체로 표기해놓은 경우도 많다.

제네릭 함수

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

만약 두개의 파라메터를 받아서 서로의 값을 swap하는 함수가 있을때 제네릭이 없다면 int, double, string등등 각 타입별로 한개씩 n개의 함수를 만들어야 할것이다.

제네릭을 이용하면 다음과 같이 사용이 가능하다.

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

제너릭 버전은 Int, String, 또는 Double 와 같은 실제 타입 이름 대신에 T 라는 임의의 이름을 사용한다 T는 정해진건 아니고 아무 문자열이나 가능하다

제네릭 구조체

이번엔 예시로 간단하게 구현한 Stack을 이용하겠다.

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

다음과 같이 int만 사용이 가능한 Stack이 있을때

제네릭으로 변경하면 다음과 같다.

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

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

사용할때 구조체 이름 뒤의 <>각괄호안에 사용하고자 하는 타입을 명시하면 된다.

제네릭 클래스

사실 형태는 구조체와 동일하다.

다음은 이전의 Stack을 클래스로 구현한 예시이다.

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

타입 제약

맨 처음에 말했듯이 제네릭이 Any와 다른점은 제약사항을 걸어서 좀더 안정성있게 다양한 타입을 받아들일 있다고 했었는데

실제로 Swift에 정의된 컬렉션 타입들중에 Dictionary를 예로들자면 이 딕셔너리의 키로 들어올 수 있는 데이터는 hashable프로토콜을 준수하는 데이터만 사용이 가능하다.

이처럼 안정성을 위해서 타입을 제약하는 일이 흔하다는것을 알고 형태를 알아보자.

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // function body goes here
}

someT는 SomeClass의 하위클래스의 인스턴스 여야 하고
someU는 SomeProtocol을 준수하는 인스턴스 여야 한다고
제약사항을 설정한 코드이다.

Associated Types

Associated Type은 프로토콜에서 제네릭을 이용하기 위한 방법이다.
아직 프로토콜을 잘모른다면 프로토콜을 보고오도록 하자.

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

Associated Types + 제약사항

확장에 사용된 제네릭에 제약사항을 추가할 수 있다.

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

이 코드는 제네릭으로 데이터를 받되 Equatable프로토콜을 준수하는 데이터만 올 수 있다고 제약사항을 설정 하고 있다.

Item: Equtable 이렇게 하면 앞서 제네릭에 타입제약을 걸었던 것처럼 제네릭에 준수해야 하는 프로토콜을 명시 할 수있다.

Where

앞서 :콜론을 사용한 제약사항은 해당 클래스의 하위클래스이거나 프로토콜을 준수하기를 명시하는것이었는데 where은 좀더 상세하게 제약을 걸 수 있다.

명심해야 할것은 :과 where은 별개의 개념이 아니다 where에서 :콜론을 이용할 수 있다. 제약조건의 상위 포지션이라고 생각하면 된다.

func areItemsEqual<T, U>(item1: T, item2: U) -> Bool where T: Equatable, T == U {
    return item1 == item2
}

이런식으로 제네릭 함수에 where을 사용해서 T는 Equatable을 준수하며 T와 U는 같은 경우에만 함수를 사용할 수 있게 설정하였다.

if문과 비슷하지만 다른점은 where을 이용하면 swift에서 컴파일 자체를 허용하지 않는다.

where을 이용한 extension

일단 extension에 대해서 모른다면 extension글을 보고 오는것을 추천한다.

extension someClass where Element: Equatable {

	//클래스 내부의 저장프로퍼티인 topItem과 파라메터로 넘겨 받은 item을 비교하는 함수
    func isTop(_ item: Element) -> Bool {
        return topItem == item
    }
}

위 코드는 someClass에서 제네릭인 Element를 확장하여 Equatable을 준수하는 데이터만 처리 할 수 있도록 확장하고 있다.

제네릭을 추가하는데 아니고 이미 정의된 제네릭에 제약조건을 추가하는 코드이다.

where을 이용한 associatedtype

앞서 프로토콜에서 제네릭을 이용하려면 associatedtype을 이용한다고 했는데

associatedtype을 이용해서 제네릭을 이용하는 경우에도 where을 이용한 상세 제약조건 설정이 가능하다.

// Container 프로토콜 정의
protocol Container {
    associatedtype Item
    var items: [Item] { get }
    func contains(item: Item) -> Bool
}

// Item 타입이 Equatable을 준수해야 하는 제약을 추가
extension Container where Item: Equatable {
    func contains(item: Item) -> Bool {
        return items.contains(item)
    }
}

이런 식으로 associatedtype에 where을 이용해서 제약조건을 추가 할 수 있다.

where을 이용한 제네릭 서브스크립트

// 제네릭 컨테이너 정의
struct Container<T> {
    var items: [T]
    
    // 서브스크립트에 where 절을 사용하여 Item 타입이 Comparable을 준수할 때만 서브스크립트 제공
    subscript<U>(index: Int) -> U? where T == U {
        return items[index] as? U
    }
}

// 사용 예시
let intContainer = Container(items: [1, 2, 3, 4, 5])
if let value: Int = intContainer[2] {
    print(value)  // 출력: 3
}

let stringContainer = Container(items: ["apple", "banana", "cherry"])
if let value: String = stringContainer[1] {
    print(value)  // 출력: banana
}

정리

swift에서 제네릭을 사용해서 코드중복을 줄일 수 있다.

함수부터 시작해서 클래스,구조체화 같은 타입 그리고 프로토콜과 서브스크립트 등에서도 제네릭을 이용할 수 있다.

제네릭에는 다양한 제약조건을 설정할 수 있다. :콜론을 이용하거나 where을 이용하면 되는데 where을 이용하면 상세한 제약조건을 설정할 수 있다.

profile
iOS 개발자 취준생, 천 리 길도 한 걸음부터

0개의 댓글