제네릭에 대해서 설명해보세요

French Marigold·2023년 7월 10일
0

면접 준비

목록 보기
9/9

제네릭이란?

제네릭은 유연한 타입을 사용하여 타입에 의존하지 않는 코드를 만들고자 할 때 사용된다.
사용하는 방법은 타입 파라미터인 T를 붙여서 사용하는 것이고,
T라는 타입엔 어떠한 타입도 들어올 수 있게된다.
"타입을 유연하게 처리하고 싶은 곳에만 T라고 표시를 해주어"
어떤 타입이든 들어오도록 만들어주면 된다.

예시를 들어보자.

var numberOne = 1
var stringOne = "One"

func putSomething<T>(something: T) -> String {
    return "지금 들어온 값은 \(something)이다."
}


// 타입 파라미터 T로 인해 Int값과 String값이 모두 들어올 수 있게 되었다.
print(putSomething(something: numberOne)) // 지금 들어온 값은 1이다.
print(putSomething(something: stringOne)) // 지금 들어온 값은 One이다.



class, struct, enum 등에도 제네릭을 사용할 수 있다.

class에 제네릭을 사용하는 방법

// class 옆에 타입 파라미터를 붙여서 해당 class는 유연한 타입을 사용할 수 있도록 하였다.

class SomeClass<T> { 
    var name: T // 타입에 의존하지 않는 저장 프로퍼티
    
    init(name: T) { // 타입에 의존하지 않는 이니셜라이저 
        self.name = name
    }
    
    
    // 타입에 의존하지 않는 인스턴스 메소드
    func doSomething(something: T) -> T {
        return "들어온 값은 \(something) 이다." as! T
    }
}


// 따라서 타입에 의존하지 않는 저장 프로퍼티에는 어떤 타입의 값도 할당할 수 있게 된다. ⭐️
let someClass1 = SomeClass(name: 4) // Int 타입 
let someClass2 = SomeClass(name: "밥") // String 타입
let someClass3 = SomeClass(name: 4.5) // Double 타입

someClass1.name // 4
someClass2.name // 밥
someClass3.name // 4.5

struct에 제네릭을 사용하는 방법

// class와 마찬가지로 옆에 타입 파라미터를 붙여서 해당 struct는 유연한 타입을 사용할 수 있도록 하였다.

struct SomeStruct<T> { 
    var something: T // 타입에 의존하지 않는 저장 프로퍼티
    
    // 타입에 의존하지 않는 인스턴스 메소드
    func doSomething(something: T) -> T {
        return "들어온 값은 \(something) 이다." as! T
    }
}


// 저장 프로퍼티가 타입에 의존하지 않으므로 어떤 타입의 값이든 들어올 수 있게 되었다.
let someStruct1 = SomeStruct(something: 100) // Int 타입 
let someStruct2 = SomeStruct(something: "치킨") // String 타입
let someStruct3 = SomeStruct(something: 5.4) // Double 타입

someStruct1.something // 100
someStruct2.something // 치킨
someStruct3.something // 5.4

enum에 제네릭을 사용하는 방법

enum에서 제네릭은 대표적으로 옵셔널의 내부적인 처리를 할 때 사용된다.
옵셔널은 다음과 같이 내부적으로 enum으로 정의되어 있다.

enum Optional<T> {
	case None
	case Some(T)
}

제네릭으로 인해서 case Some의 값에는 어떤 값이든 들어올 수 있게 되었다.




제네릭의 타입 제약

제네릭에서는 특정 타입만 들어올 수 있도록 타입을 제약할 수 있다.

1) 프로토콜 제약: 특정 프로토콜을 준수하는 타입만 받을 수 있게 제약을 건다.

func typeIsEqual<T: Equatable>(a: T, b: T) -> Bool{
    return a == b // Error!
}

위 함수에 문제가 발생하는 이유는 범용 타입인 T가 Equatable을 준수하지 않는 타입일 수도 있기 때문이다.
쉽게 말해 파라미터 a와 b에는 String, Int와 같이
Equatable을 준수하는 타입만 들어오는 것이 아니라
class, struct와 같이 Equatable을 준수하지 않는 타입도 들어올 수 있지 않은가?
즉, 이 때 제네릭에 Equatable 프로토콜을 준수하는 범용타입 T만 들어올 수 있게 만들어주면 비로소 파라미터 a와 b를 비교할 수 있게 되는 것이다.


2) 클래스 제약: 특정 클래스만 받을 수 있게 제약을 건다.

class MyClass { var name = "나" }
class YourClass { var name = "너" }
class OurClass { var name = "우리" }

// MyClass만 받을 수 있도록 제약을 걸었다.
func printSome<T: MyClass>(a: T) {
    print(a.name)
}

let myClass = MyClass()
let yourClass = YourClass()
let ourClass = OurClass()

// 다른 class가 들어오면 에러가 뜬다. 타입에 제약을 걸었기 때문이다. 
printSome(a: yourClass) // Error!

printSome(a: myClass) // 나



프로토콜 내부에서 제네릭을 사용하는 방법

원래 사용하던 제네릭 문법 대신 associatedtype T를 사용한다.

protocol Controller {
    associatedtype T
    func putSomething() -> T
}

class StringController: Controller {
    
    // 프로토콜을 구현할 때 개발자가 원하는 타입을 작성하면 된다.
    func putSomething() -> String {
        return "값이 나옴"
    }
}

class IntController: Controller {
    
    // 프로토콜을 구현할 때 개발자가 원하는 타입을 작성하면 된다.
    func putSomething() -> Int {
        return 30
    }
}

let stringController = StringController()
let intController = IntController()

print(stringController.putSomething()) // 값이 나옴
print(intController.putSomething()) // 30
profile
꽃말 == 반드시 오고야 말 행복

0개의 댓글