Rx를 들어가기 전! 제네릭에 대해서 복습 겸 한 번 정리해 보자고!

제네릭.. 이름부터 간지 철철❗️

 

애플 말에 따르면 Swift에서 가장 강력한 기능 중 하나가 바로 제네릭인데,

이유는 바로! Swift 표준 라이브러리의 대다수는 제네릭으로 선언되어 있기 때문임.

 

제네릭의 기본개념

제네릭은 코드의 중복을 줄이고, 타입에 안전한 코드를 작성할 수 있도록 도와줌.

예를 들어, 두 값을 서로 바꾸는 함수를 비교해 보자.

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

 

평소에 우리가 제네릭을 쓰지 않는다면 이런 식으로 짤 것임.

하지만, 요 func는 Int밖에 들어가지 않지?

 

그럼, Double, String, 등등 다른 타입이 올 경우엔 어떡하지?

제네릭이 없다면 하나하나 파라미터 값을 변경하여 메서드를 생성해 주어야 함..

 

하지만! 우리에겐 제네릭이 있지. 후후

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

 

요렇게! 타입에 제한을 두지 않고 쓸 수 있다!

여기서 <T>는 너 T야!? 할 때 그 T가 아니라,

 

타입 파라미터(Type parameter)라고 부름.

타입 파라미터는 임의의 타입을 지정하고 이름을 지정하여 꺾쇠괄호 사이에 기록하고,

함수의 이름 바로 뒤에 작성함

 

아무튼, 이렇게 swapTwoValues라는 제네릭으로 선언해 주면,

var testNumA = 1
var tsetNumB = 2
swapTwoValues(&testnumA, &testnumB)	// int type

var testStringA = "Hi"
var testStringB = "Hello"
swapTwoValues(&testStringA, &testStringB) // string type

 

이렇게 실제 함수를 호출할 때

제네릭의 타입 파라미터 <T>의 값이 정해지는 거고,

 

만약 서로 다른 타입을 전달하게 되면,

swapTwoValues(&testNumA, &testStringA)

Cannot convert value of type 'String' to expected argument type 'Int'

 

요런 에러가 뜨게 되는 거지.

 


 

제네릭 타입

다음으로 위에서 배운 개념을 이용한 함수를 제네릭 함수 라고 하는데,

이 제네릭은 함수뿐만 아니라 구조체, 클래스, 열거형 타입에도 선언이 가능함.

이를 제네릭 타입이라고 한다.

 

첫 번째로 제네릭 구조체에 대해서 알아보자.

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

 

이렇게 제네릭 타입으로 스택을 선언할 수 있다.

 

++++++

여기에서, mutating func가 뭘까?

struct or enumeration 내에서 인스턴스 메서드가 해당 인스턴스의 프로퍼티 수정을

가능하게 만들어 줌.

 

그러면 왜 'mutating' 키워드를 쓰는 건데?

구조체와 열거형값 타입임. 기본적으로 값 타입의 인스턴스 메서드는

인스턴스 프로퍼티를 변경할 수 없음.

하지만, mutating을 사용하면 메서드 내에서 인스턴스의 프로퍼티 변경이 가능해짐!

 

다음으로, 제네릭 클래스에 대해서 알아보자.

public class Box<T> {
    private T item;
    
    public void set(T item) {
        this.item = item;
    }
    
    public T get() {
        return item;
    }
}

요건 자바코드긴 한데,

예시로 보여주기 너무 좋아서 가지고 와봄.

이런 식으로 'Box'클래스는 'T'라는 제네릭 타입 매개변수를 가지고,

이렇게 박스가 다양한 타입의 아이템을 가질 수 있도록 선언이 가능함.

 

마지막으로, 제네릭 열거형 타입임

enum Result<Value> {
    case success(Value)
    case failure(Error)
}

 

이렇게 Value라는 제네릭 타입 매개변수를 쥐어주면, 성공 케이스가

다양한 타입의 값을 가질 수 있어짐

 


 

제네릭 타입 제약

이번에 제네릭을 찾아보면서 좀 중요할 것 같은데? 싶은 부분이었음.

(어렵다 -> 중요하다) ㅋㅋ

이제, 제네릭을 사용할 때 타입제약을 추가하여 안전하고 의도에 맞는

타입을 사용할 수 있도록 제약을 두는 것임!

 

구조체에서의 타입제약

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

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

    mutating func pop() -> Element? {
        return items.popLast()
    }
    
    func contains(_ item: Element) -> Bool {
        return items.contains(item)
    }
}

 

보면 'Element'옆에 'Equatable'이라는 것이 추가되었음

이게 뭐냐 하면 예를 들어, 우리가 파라미터로 두 개의 값을 받아서

두 값이 같으면 ture, 아니면 false를 반환하는 함수를 제네릭으로 선언하려고 할 때 어떻게 할 수 있을까?

 

func isSameValues<T>(_ a: T, _ b: T) -> Bool {
    return a == b
}

 

이런 식으로 쓰겠지?

하지만 이렇게 쓰면 다음과 같은 오류가 발생함.

Binary operator '==' cannot be applied to two 'T' operands

 

왜냐하면, == 이란 연산자는 a와 b의 타입이 Equatable이란 프로토콜을 준수할 때만 사용이 가능하기 때문임.

그렇기 때문에? <Element: Equatable> 이런식으로 제약을 주는 것이다!

 

클래스에서의 타입제약

class Animal { }
class Cat { }
class Dog: Animal { }

func animalShelter<T: Animal>(_ a: T) { }

 

이렇게 T: Animal처럼 클래스 이름을 생성한 뒤에

 

let animal = Animal.init()
let dog = Dog.init()
let cat = Cat.init()

animalShelter(animal)
animalShelter(dog)
animalShelter(cat)

 

요런 식으로 호출하면, Animal클래스 인스턴스인 animal과 Dog는

animalShelter이란 제네릭 함수를 실행시킬 수 있지만,

cat은 불가능해짐.

 

마지막으로 열거형에 타입제약은

enum Result<Value: Decodable> {
    case success(Value)
    case failure(Error)
}

 

이런 식으로 Value가 Decodable 프로토콜을 준수하는 타입이어야 한다 라는

제약을 걸 수도 있음

 

이렇게 제약조건까지 알아봤슴미당!

다음에 봐요~

'Swift' 카테고리의 다른 글

Swift RxSwift  (0) 2024.08.01
Xcode Instruments  (0) 2024.07.31
Swift MVVM  (0) 2024.07.19
Swift 싱글톤 패턴  (0) 2024.07.14
(CS) 네트워크 대비 지식쌓기 (2/2)  (0) 2024.07.12