iOS/Swift

Swift 에서의 Generics(제너릭)

태애니 2025. 3. 20. 20:43
728x90

 

 

 

Generic이란

특정 타입에 의존하지 않고, 다양한 타입에서 동작할 수 있도록 구현하는 기능을 말한다.

이를 이용하면 코드의 재사용성과 타입 안정성을 높일 수 있다.

 

Swift에서는 이를 함수, 메서드, 클래스, 구조체, 열거형 등 다양한 곳에서 사용이 가능하다.

 

 

쉽게 말해서 여러 타입의 값이 들어왔을 때 이를 규정하지 않고 다양하게 받아 처리하게 해준다는 뜻이다.

 

  1. 제너릭 함수
    • inout 매개변수
    • 튜플을 이용한 값 교환
  2. 제너릭 메서드
    • 구조체에서의 제너릭 메서드
  3. 제너릭 클래스
    • 제너릭을 이용한 클래스 설계
  4. 제너릭 열거형
    • 제너릭을 활용한 Result 타입
  5. 제너릭 구조체
    • 여러 개의 타입을 받는 구조체
    • 동일한 타입을 제한하는 Comparable 사용
  6. 제너릭과 프로토콜
    • associatedtype을 활용한 프로토콜 정의
  7. 프로토콜과 제너릭
    • DictionaryWrapper를 통한 프로토콜과 제너릭 활용
  8. 제너릭 서브스크립트
    • subscript를 이용한 값 접근
  9. 제너릭 확장
    • 기존 구조체 확장에서 제너릭 활용

 

 


 

1. 제너릭 함수 처리

// T : 타입 파라미터이며, 함수가 호출될 때 전달된 타입으로 대체
func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

var x = 10
var y = 20

// &x, &y : inout 매개변수로 값을 전달할 때, 해당 변수의 메모리 주소를 직접 참조하도록 표시
swapValues(&x, &y)
print(x, y)  // 20, 10

var str1 = "Hello"
var str2 = "World"
swapValues(&str1, &str2)
print(str1, str2)  // "World", "Hello"

 

inout(&) 매개변수는 기본적으로 let이 아닌 var 로 전달해야 한다.
또한 함수 내에서 복사되지 않고 직접 수정되므로, 함수가 종료될 때까지 원본 변수에 접근할 수 없다.

 

func testInout(_ a: inout Int, _ b: inout Int) {
    a = b  // 가능
    // b = a  // 같은 변수를 중복해서 사용 할 수 없으므로 오류가 남.
}

 

만약 & 쓰기 싫은데 -.-? 할 수 도 있는데, 튜플 방식을 사용해도 된다고 한다.

근데.. 그냥 & 쓰는게 낫지 않을까?🤔

// & 쓰기 싫어요.

func swapValues<T>(_ a: T, _ b: T) -> (T, T) {
    return (b, a)
}

var x = 10
var y = 20
(x, y) = swapValues(x, y)
print(x, y)  // 20, 10

 

 

 

 


 

2. 제너릭 메서드 

struct Stack<Element> {
    private var items: [Element] = []

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

    mutating func pop() -> Element? {
        return items.popLast()
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2) // [1, 2] 가 들어있다.
print(intStack.pop() ?? "pop할게 없어!")   // 1이 pop 된다 -> print에는 2
print(intStack.pop() ?? "pop할게 없어!")   // 2가 pop 된다 -> "pop할게 없어!"

 

Element Stack이 사용할 데이터 타입을 의미하며, 실행 시점에 결정된다.
push() pop() Element 타입을 사용하여 다양한 타입의 데이터를 다룰 수 있다.

 

 


 

3. 제너릭 클래스

// 다양한 타입을 처리할 수 있는 클래스

class Box<T> {
    var value: T
    
    init(_ value: T) {
        self.value = value
    }
    
    func getValue() -> T {
        return value
    }
}

let intBox = Box(1234)
print(intBox.getValue()) // 1234

let strBox = Box("Hello, World")
print(strBox.getValue()) // "Hello, World"
<T> : 타입 파라미터
value: T : T 타입의 프로퍼티를 가짐
getValue() : T 타입의 값을 반환함

 

 

 


 

 

4. 제너릭 열거형

// 제너릭 열거형

enum Result<T> {
    case success(T)
    case failure(String)
}

let successResult: Result<Int> = .success(100)
let failureResult: Result<String> = .failure("Error Occurred")

switch successResult {
case .success(let value):
    print("Success with value \(value)")
case .failure(let message):
    print("Failure: \(message)")
}
<T> : 성공했을 때 T 타입의 데이터를 담음
.success(T), .failure(String) : 서로 다른 타입을 받아서 처리

 


 

5. 제너릭 구조체

 

struct Pair<T, U> {
    let first: T
    let second: U
}

let pair = Pair(first: "Apple", second: 3500)
print(pair.first)  // "Apple"
print(pair.second) // 3500
<T, U> : 두 개의 타입을 다룰 수 있다고 정의해 둔 것이다.

 

 

만약, 두 개의 값은 같게 받고 싶다면?

 

func findLargest<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

print(findLargest(10, 20))    // 20
print(findLargest("apple", "banana"))  // "banana"

 

<T: Comparable> : T가 Comparable 프로토콜을 준수해야 함을 의미
a > b 비교가 가능해야 하므로 제한을 걸어줌

 


 

 

6. 제너릭과 프로토콜

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

struct IntContainer: Container {
    var items: [Int] = []
    
    mutating func append(_ item: Int) {
        items.append(item)
    }
    
    func count() -> Int {
        return items.count
    }
}

var container = IntContainer()
container.append(5)
print(container.count())  // 1

 

associatedtype Item : 프로토콜 내부에서 사용할 타입을 정의함
IntContainer는 Int 타입을 가지도록 구현한다

 


 

7. 프로토콜과 제너릭 

 

struct DictionaryWrapper<Key: Hashable, Value> {
    private var dict: [Key: Value] = [:]
    
    subscript(key: Key) -> Value? {
        get { return dict[key] }
        set { dict[key] = newValue }
    }
}

var myDict = DictionaryWrapper<String, Int>()
myDict["age"] = 25
print(myDict["age"]!)  // 25
associatedtype Item 을 사용하여 프로토콜 내부에서 사용할 타입을 정의
IntContainer 는  Int타입을 가지도록 구현한다는 의미

 


 

8.제너릭 서브스크립트

 

struct DictionaryWrapper<Key: Hashable, Value> {
    private var dict: [Key: Value] = [:]
    
    subscript(key: Key) -> Value? {
        get { return dict[key] }
        set { dict[key] = newValue }
    }
}

var myDict = DictionaryWrapper<String, Int>()
myDict["age"] = 25
print(myDict["age"]!)  // 25
Key: Hashable 을 사용하여 키가 Hashable을 준수하도록 제한
subscript(key: Key) -> Value? 를 사용하여 키를 이용해 값에 접근함.

 

 


 

9. 제너릭 확장

 

extension Stack {
    var topItem: Element? {
        return items.last
    }
}

var stack = Stack<Int>()
stack.push(10)
stack.push(20)
print(stack.topItem)  // 20
Stack 구조체에 topItem 프로퍼티를 추가하여 마지막 요소를 반환하는 기능
-> Element 타입을 그대로 사용하여 유연성을 유지함.

 

 

 


 

associatedtype은 프로토콜에서 사용할 타입을 추후에 지정하도록 하는 기능.
제너릭(Generic)과 비슷한 개념이지만, 프로토콜에서 특정 타입을 미리 정하지 않고, 이를 채택하는 타입이 정의하도록 함

-> 더 유연한 설계가 가능

 

 

subscript는 배열(Array)처럼 인덱스를 사용하여 값에 접근할 수 있도록 해주는 기능.
컬렉션, 리스트, 사전(Dictionary) 같은 타입에서 값을 인덱스 또는 키를 통해 읽고 쓰는 방법을 제공.
// associatedtype 선언 예제
protocol Container {
    associatedtype Item  // 연관 타입 선언
    var items: [Item] { get set }
    mutating func append(_ item: Item)
    func getItem(at index: Int) -> Item
}



// subscript 기본 예제
subscript(index: Int) -> 타입 {
    get {
        // 반환할 값 정의
    }
    set(newValue) {
        // 새 값 저장하는 코드
    }
}

 

728x90