본문 바로가기
Xcode/Swift - PlayGround

PlayGround) 제너릭에 대해 알아보자

by 후르륵짭짭 2021. 3. 27.
728x90
반응형

 

안녕하세요! 짭짭이 입니다.

정말 오랜만에 글씁니다. ㅎㅎㅎ

최근에 너무 바빠서 글을 포스팅 할 시간이 없었습니다. 

스스로에게 반성합니다. 훨씬 더 성장해야합니다.

아직 완벽한 것은 아닌데, 제너릭에 대해 공부한 것을 적어보려 합니다.

 

** Generics란 **

Apple Document를 보면 Generic은 여러분이 정의한 타입에 대해 유연성을 가능하게 해주는 겁니다.

아래와 같이 두개의 대상을 Swap 하는 코드가 있다고 할 때, 평소와 같이 한다면 하나의 타입에 대해서만 적용이 됩니다.

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

하지만 만약 제너릭을 사용한다면 특정 타입에 제한 되는 것이 아니라, 아래 처럼 타입에 관계없이 사용 할 수 있습니다.

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

기본적인 구조는 정말 간단합니다.

func swap< 제너릭 타입 이름 >(item1 : inout 제너릭 타입 이름 , item2 : inout 제너릭 타입 이름){
    let temp = item1
    item1 = item2
    item1 = temp
}

<> 안에 제너릭 이름을 넣어주면 됩니다.

보통 이름이 생각 안난다면 T, U, V 를 사용한다고 합니다 ㅎㅎㅎ.

그렇다면 아래와 같이 작성 했을 때, 결과도 어떨지 한번 보도록 하겠습니다.

//Generic Type Function - Swap
func swap<Element>(item1 : inout Element , item2 : inout Element){
    let temp = item1
    item1 = item2
    item1 = temp
}

var A : String = "ChapChap"
var B : String = "Hururuek"

print("\(A) , \(B)") //결과 : ChapChap , Hururuek

swap(&A, &B)

print("\(A) , \(B)") //결과 : Hururuek , ChapChap

이렇게 제너릭을 사용한다면 유연한 코드를 작성 할 수 있게 됩니다.

 

** Genric with Struct **

제너릭은 함수 뿐만 아니라 클래스 프로토콜 , 구조체 등 다양한 부분에서 적용할 수 있습니다.

일단 구조체에 사용된 구조체를 보도록 하겠습니다.

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

제너릭을 적용한 구조체를 만들 때는 구조체이름<제너릭 타입 이름> 이렇게 해주면 됩니다.

그러면 <>에 해당하는 제너릭 타입에 맞게 타입을 생성하게 됩니다.

그러면 사용하는 방법도 필요합니다.

var stack : Stack<String> = Stack<String>()

stack.push("Hello")
stack.push("World")

print(stack.items) //결과 : ["Hello", "World"]

stack.pop() // 결과 : "World"

사용 할 때는  구조체이름<사용할 타입>() 이렇게 해줘야합니다.

보통은 구조체이름() 이렇게 됐지만 제너릭이 들어가기 때문에 구조체이름<>() 이런 형식이 되어야 합니다.

 

뿐만 아니라 아래 처럼 Extension을 사용해서 확장 할 수도 있습니다.

이때, 제너릭 타입을 그대로 사용할 수 있다는 특징이 있습니다.

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

print(stack.topItem ?? "비었습니다") // 결과 : Hello

 

** Genric with Type Constraint **

유연한 타입 사용을 위해서 제너릭에 타입에 관계없이 사용할 수 있다는 장점이 있지만

특정 상황에서만 사용해야할 제너릭 타입이 필요 할 때가 있을 겁니다.

그럴 때 제약조건을 제너릭에 줄 수 있습니다.

func compareFunction< Element : Equatable>(item : Element , list : [Element]) -> Int? {
    
    for (index , value) in list.enumerated(){
        
        if value == item {
            return index
        }
        
    }
    
    return nil
}

let list : [Int] = [3,1,4,5,1,5,1,0,9,5]

print(compareFunction(item: 1, list: list) ?? "없습니다") // 결과 : 1

기본에 사용 했던 제너릭 타입과 차이점은 

제너릭 타입 이름 선언하는 <> 부분에

< 제너릭이름 : 제약조건 > 이렇게 들어갔습니다.

이 의미는 해당하는 제너릭은 제약조건을 만족해야 사용할 수 있는 것을 의미합니다.

그리고 이 제약 조건은 클래스와 프로퍼티가 가능합니다.

위의 코드는 프로퍼티에 해당하는 제약 조건을 걸어 준 것이고 

클래스의 제약 조건은 아래와 같이 줄 수 있습니다.

class Animal {
    
    var name : String
    
    init(name : String){
        self.name = name
    }
    
}

class Person : Animal {
    
    var age : Int
    
    init(name : String , age : Int){
        
        self.age = age
        
        super.init(name: name)
    }
    
}

func printName <Element : Animal>(input : Element){
    
    if let item = input as? Person {
        print("name : \(item.name) age : \(item.age)")
    }
    else{
        print("name : \(input.name)")
    }
    
}

let animal : Person = Person(name: "ChapChap", age: 27)
printName(input: animal) // 결과 : name : ChapChap age : 27

 

** associatedtype 이란? **

Associated Type은 Protocol에 제너릭 타입을 지정해주는 거라 생각 하면 됩니다.

위에 정의문에서도 associated type 은 protocol의 한 부분으로 사용되는 타임에 이름을 주는 것이라고 되어 있습니다.

예를들면 다음 Protocol에 Item이 제너릭 타입이고 그 Item을 각 함수에서 사용하고 있습니다.

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

그럼 위에 Protocol을 각 구조체에 상속 받도록 하겠습니다.

struct IntStack : Container {
    
    var items : [Int] = []
    
    mutating func push(_ item : Int){
        items.append(item)
    }
    
    mutating func pop() -> Int {
        return items.removeLast()
    }
    
    typealias Item = Int
    
    mutating func append(_ item: Int) {
        self.push(item)
    }
    
    var count: Int{
        return items.count
    }

}

이렇게 하면 typealias 라는 이름으로 associatedType이 생성이 됩니다.

그리고 그 부분에 Int를 넣어주면 함수에 item : Item 부분이 Int로 바뀌게 됩니다.

이렇게 typealias로 associatedtype을 지정해 줄 수 있지만 

아래 처럼 <제너릭 이름>을 해주면 typealias가 생기지 않고 

자동으로 associatedtype을 알아차립니다.

struct Stacks<Element> : Container {

    var items = [Element]()
    
    mutating func push(_ item : Element){
        items.append(item)
    }
    
    mutating func pop() -> Element{
        return items.removeLast()
    }
    
    mutating func append(_ item: Element) {
        self.push(item)
    }
    
    var count: Int{
        return items.count
    }
    
}

 

** Generic에 where로 제약 조건 추가 ** 

현제 제너릭에 제약 조건을 추가할 때는 <제너릭 : 제약조건> 이렇게 하나 밖에 할 수 없지만

where를 사용하면 여러개를 추가 할 수 있습니다.

protocol Rules {
    
    associatedtype Item
    
    var list : [Item] {get set}
    
    var count : Int {get}
    
}

func allItemsMatch<C1: Rules, C2: Rules>(_ someContainer: C1, _ anotherContainer: C2) -> Bool where C1.Item == C2.Item, C1.Item : Equatable {

        // Check that both containers contain the same number of items.
        if someContainer.count != anotherContainer.count {
            return false
        }

        // Check each pair of items to see if they're equivalent.
        for i in 0..<someContainer.count {
            if someContainer.list[i] != anotherContainer.list[i] {
                return false
            }
        }

        // All items match, so return true.
        return true
}

이렇게 Rule이라는 Protocol을 만들고 

allItemsMatch 함수를 보면 각 제너릭은 Rules를 상속 받으면서

제너릭의 associatedType인 C1.Item과 C2.Item은 같아야 하며, 해당하는 associatedType이 Equatable을 준수해야합니다.

이렇게 where를 사용하면 제너릭에 여러가지 제약 조건을 추가 할 수 있습니다.

만약 아래 처럼 제약 조건을 위배 했을 경우에는 , 아래 처럼 오류가 발생 합니다.

 

** Extension으로 확장 하기 **

제너릭도 역시 Extension으로 확장 할 수 있는데, 이때 where로 제약 조건을 걸어주면 

Extension에 존재하는 함수들을 사용 할 때는 where 조건을 만족할 때만 사용 할 수 있다.

extension Stacks where Element : Equatable {
    
    func isTop(_ item: Element) -> Bool {
           guard let topItem = items.last else {
               return false
           }
           return topItem == item
    }
    
}

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // Error

위에 처럼 Stack과 같은 구조체나 클래스에 extension을 해서 확장 한 것 처럼

Protocol도 extension으로 확장 할 수 있다.

//조건을 넣어줄 때 재료 : 조건
extension Container where Item: Equatable {
    func startsWith(_ item: Item) -> Bool {
        return count >= 1
    }
}

//Protocol일 경우에는 associatedtype에 대해서 제한을 줄 수 있다.
extension Container where Item == Int{
    
    func average() -> Double {
          return Double(count) / Double(2)
       }
    
}

만약 Protocol에 associatedtype을 사용 했다면 위에 처럼 Item에 where로 제약사항을 넣어 줄 수 있다.

이렇게 extension을 통해 제너릭을 확장 할 수 있고 

특정 부분에 where로 제약조건을 걸어서 좀 더 다양하게 활용 할 수 있다.

 

지금 까지 제너릭에 대해 공부 했는데,,,,

사실 아직 부족한 부분이 많다.

이 외에도 서브스크립트 와 associatedtype에 제약조건을 걸어주는 것 등 많지만

그 부분에 대해서 나중에 좀더 깊게 공부 해봐야겠다.

감사합니다

728x90
반응형

댓글