본문 바로가기

iOS 앱 개발자 프로젝트/Swift 문법 정복하기

[Swift] Generic

함수, 타입, 데이터 구조에 대한 유연하고 추상적인 코드를 작성할 수 있게 해주는 기능.

다양한 타입에서 작동하도록 일반화된 코드를 작성할 수 있게 해주는 제네릭  을 공부해 봅니다. 


 

☑️  Generic

 

  • 제네릭은 실제 타입 이름을 써주는 대신에 placeholder를 사용한다. [ eg: T, V, U ]
  • placeholder의 실제 타입은 함수가 호출되는 순간 결정된다.
  • placeholder는 타입 매개변수로 쓰일 수도 있는데, 이 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환된다.
  • 하나의 타입 매개변수를 갖지 않고 여러 개의 타입 매개변수를 갖고 싶다면 홀화살괄호 기호 안쪽에 쉼표로 분리한 여러 개의 타입 매개변수를 지정해줄 수 있다. [ eg: <T, U> ]
  • 제네릭 타입을 구현하면 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있다.
  • 제네릭 타입을 정해주면 그 타입에만 동작하도록 제한할 수 있어 안전하게 기능이 사용되도록 유도할 수 있다.
  • 제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 있다.
  • 제네릭을 사용하고자 할 때는 제네릭이 필요한 타입 또는 메서드의 이름 뒤의 홀화살괄호 기호 사이에 제네릭을 위한 타입 매개변수를 써주어 제네릭을 사용할 것임을 표시한다.
/*
inout 키워드는 함수 내에서 매개변수로 전달된 값을 변경하고, 
이를 함수 외부에서도 반영할 수 있도록 하는 데 사용됩니다. 
이를 통해 함수 내에서 매개변수의 값을 직접 수정할 수 있습니다.

inout 키워드 사용 방법:
1. 매개변수에 inout 키워드를 붙여 선언합니다.
2. 함수 호출 시 매개변수를 & 기호로 전달하여 해당 값을 참조로 전달합니다.
*/


// 함수 정의
func increment(_ value: inout Int) {
    value += 1
}

var number = 5
print("Before increment: \(number)") // 출력: Before increment: 5

// 함수 호출 시 매개변수에 &를 사용하여 변수의 참조를 전달
increment(&number)

print("After increment: \(number)") // 출력: After increment: 6

 

// 두 변수의 값을 바꿔주는 함수를 타입별로 작성해야함(제네릭 사용 X)
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

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

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 제네릭을 사용하면 타입에 상관없이 사용가능함
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}
// 이전에 배웠던 큐, 스택을 다시 살펴보자
struct Queue<T> {
    private var queue: [T] = []
    
    public var count: Int {
        return queue.count
    }
    
    public var isEmpty: Bool {
        return queue.isEmpty
    }
    
    public mutating func enqueue(_ element: T) {
        queue.append(element)
    }
    
    public mutating func dequeue() -> T? {
        return isEmpty ? nil : queue.removeFirst()
    }
}

var queue = Queue<Int>()
queue.enqueue(10)
queue.enqueue(20)
queue.dequeue() // 10

struct Stack<T> {
    private var stack: [T] = []
    
    public var count: Int {
        return stack.count
    }
    
    public var isEmpty: Bool {
        return stack.isEmpty
    }
    
    public mutating func push(_ element: T) {
        stack.append(element)
    }
    
    public mutating func pop() -> T? {
        return isEmpty ? nil : stack.popLast()
    }
}

var stack = Stack<Int>()
stack.push(10)
stack.push(20)
stack.pop() // 20
/*
where 키워드는 무엇일까요?

제네릭의 제약조건(Constraints)인 where 키워드는 제네릭 타입에 특정 조건을 부여하여 
해당 제약을 충족하는 타입만을 사용할 수 있도록 하는 기능입니다. 
where 키워드를 사용하여 제네릭 타입에 특정 프로토콜 채택, 
특정 타입과의 상속 관계 등을 제한할 수 있습니다.
*/

// 프로토콜 채택 제약 예시
func process<T>(value: T) where T: Numeric {
    // Numeric 프로토콜을 채택하는 타입만을 제네릭 타입 T로 받음
    print("Value is a numeric type.")
}

process(value: 5) // 출력: Value is a numeric type.
process(value: 3.14) // 출력: Value is a numeric type.
// process(value: "Hello") // 컴파일 에러 - 문자열은 Numeric 프로토콜을 채택하지 않음


// 클래스의 상속 관계 제약 예시
class MyClass {}
class MySubclass: MyClass {}

func process<T>(value: T) where T: MyClass {
    print("Value is an instance of MyClass or its subclasses.")
}

let obj = MySubclass()
process(value: obj) // 출력: Value is an instance of MyClass or its subclasses.
// process(value: "Hello") // 컴파일 에러 - 문자열은 MyClass 또는 그 하위 클래스가 아님