2023년 1월 1일
08:00 AM
Buffering ...

최근 글 👑

[Swift-Study] 심화 문법종합반 2주차 5일차 정리 - 제네릭

2024. 3. 27. 20:30ㆍIOS/Swift-Study
SMALL

제네릭

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

다양한 타입에서 작동하도록 일반화된 코드를 작성을 도와줍니다.

특징

- 제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 있음

- 제네릭을 사용하고자 할 때는 제네릭이 필요한 타입 또는 메서드의 이름 뒤의

화살괄호 기호 사이에 제네릭을 위한 타입 매개변수를 써주어 제네릭을 사용할 것임을 표시

- 제네릭은 실제 타입 이름을 써주는 대신에 placeholder를 사용 [ eg: T, V, U ]

- placeholder는 타입의 종류를 알려주지 않지만 어떤 타입이라는 것은 알려줌

- placeholder의 실제 타입은 함수가 호출되는 순간 결정

- placeholder는 타입 매개변수로 쓰일 수 있음, 이 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환

- 하나의 타입 매개변수를 갖지 않고 여러 개의 타입 매개변수를 갖고 싶다면

홀화살괄호 기호 안쪽에 쉼표로 분리한 여러 개의 타입 매개변수를 지정가능 [ eg: <T, U> ]

- 제네릭 타입을 구현하면 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작 가능

- 제네릭 타입을 정해주면 그 타입에만 동작하도록 제한할 수 있어 안전하고 의도한 대로 기능을 사용하도록 유도 가능

/*
inout 키워드는 함수 내에서 매개변수로 전달된 값을 변경,
이를 함수 외부에서도 반영할 수 있도록 하는 데 사용
이를 통해 함수 내에서 매개변수의 값을 직접 수정 가능

inout 키워드 사용 방법:
1. 매개변수에 inout 키워드를 붙여 선언
2. 함수 호출 시 매개변수를 & 기호로 전달하여 해당 값을 참조로 전달
*/
/*
 increment 라는 함수를 만들어주고, value를 받아 value에 1을 더해줍니다.
 value 파라미터에는 inout이 선언이 되어 있습니다.
 즉, value를 외부에서 받아서 원본 자체에 +1을 해주는 것입니다.
*/

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

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

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

print("After increment: \(number)") // 출력: After increment: 6
// 두 변수의 값을 바꿔주는 함수를 타입별로 작성해야함(제네릭 사용 X)
/*
 2개의 Int 파라미터를 받아 2개의 값을 바꿔 줍니다.
 2개의 값을 바꿔주려면 중간자의 역할을 하나 만들어 줘야 합니다.
 하나의 변수 혹은 상수에 a라는 값을 담고 a에 b를 담고 b에다가 저장 해둔 a를 담으면,
 2개의 값이 바뀌게 됩니다. 이러한 로직을 사용하면 웬만한 타입들은 적용 됩니다.
 
 하지만 바꾸고 싶은 타입이 여러개일 경우엔 계속 함수를 만들어 주어야 하는 불편함이 있습니다.
 아래 변경되는 부분들은 전부 타입에 불과 합니다. 그렇기 때문에 타입을 제네릭으로 지정을 해준다면
 호출시에 타입이 결정이 되는 것이기 때문에 제네릭을 사용하여 하나의 함수로도 모든 케아스를 커버 해줄 수 있습니다.
*/
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> {" 여기서 정의된 <T>는 
 "private var queue: [T] = []" 여기에 있는 queue의 [T]와 일치합니다.
 그리고 "enqueue(_ element: T) {" 이구문의 "enqueue" 즉, queue에다 넣는 행위를 할 때,
 파라미터로 받는 T를 말합니다. 타입도 일치합니다.
 "dequeue"도 옵셔널T 일치합니다.
 
 그렇기 때문에 queue를 만들 때 "var queue = Queue<Int>()" <>(꺽쇠) 안에 넣고 싶은 타입을 넣어서
 자유롭게 사용할 수 있었습니다.
*/
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) // Int
queue.enqueue(20) // Int
queue.dequeue() // 10


// Stack도 마찬가지 입니다.
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
// 딕셔너리 예시
/*
 딕셔너리도 Key와 Value의 타입으로 자유롭게 선언이 가능했습니다.
 이유는 Dictionary에서 제네릭을 구현 하고 있어서 입니다.
*/
@frozen public struct Dictionary<Key, Value> where Key : Hashable {

    /// The element type of a dictionary: a tuple containing an individual
    /// key-value pair.
    public typealias Element = (key: Key, value: Value)


var fruitsInventory: Dictionary<String, Int> = [:]
fruitsInventory["apple"] = 3
/*
Key, Value 타입의 '제네릭'으로 되어있어서 자유롭게 원하는 타입으로 딕셔너리를 생성할 수 있음
제약조건은 Key가 Hashable 프로토콜만 따르면 되는 것이며
기본 자료형인 String은 Hashable 프로토콜을 따르고 있습니다.
만약 다른 자료형을 Key로 사용하려면 Hashable 프로토콜을 채택해야 합니다.
*/
/*
where 키워드는 무엇일까요?

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

// 프로토콜 채택 제약 예시
/*
 process라는 함수에 T라는 플레이스 홀더가 있고, 
 "where T: Numeric { " T라는 타입의 Numeric이라는 프로토콜을 따라야 하고 있습니다.
 그래서 Value에 5가 들어갔을 때 Int는 Numeric 프로토콜을 채택을 하고 있습니다.
 그렇기 때문에 print가 실행이 되는 것 입니다.
 
 3.14도 동일하게 Numeric 즉, 숫자라는 것이죠 숫자 프로토콜을 따르고 있기 때문에 실행이 되는 것 입니다.
 하지만 (value: "Hello") 를 했을 때에는 Numeric 프로토콜을 채택하지 않기 때문에 사용 할 수 없습니다.
 라는 제약 조건을 가지고 있는 것입니다.
*/

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() // MySubclass는 MyClass를 상속받은 클래스입니다.
// 하지만 Hello 라는 문자열을 넣게 되면 MyClass를 상속받거나 혹은 MyClass 본인 자체가 아니기에 에러가 발생합니다.
process(value: obj) // 출력: Value is an instance of MyClass or its subclasses.
// process(value: "Hello") // 컴파일 에러 - 문자열은 MyClass 또는 그 하위 클래스가 아님
728x90