티스토리 뷰

programming/Swift

[Swift/iOS] 제네릭(1)

마들브라더 2022. 10. 21. 16:14

제네릭

  • 여러 타입에 유연하게 대응할 수 있음
  • 재사용하기 쉽고 코드의 중복을 줄일 수 있음
    • 깔끔하소 추상적인 표현 가능해짐
  • 많은 표준 라이브러리가 수많은 제네릭 코드로 구성되어 있음
  • 제네릭을 사용할 때는 제네릭이 필요한 타입 또는 메서드의 이름뒤의 <>사이에 타입 매개변수를 써주어 제네릭을 사용할 것임을 표시함
    • (제네릭을 사용하고자 하는 타입 이름) <타입 매개변수>
    • (제네릭을 사용하고자 하는 함수 이름) <타입 매개변수> (함수의 매개변수 … )
  • Array 타입
    • Array는 타입 매개변수 Elemnet가 있고, map 메서드는 매개변수 T가 있음
    • Array는 제네릭을 사용하는 제네릭 타입, map 메서드는 제네릭을 사용하는 제네릭 함수
public stuct Array<Element> : // 생략
// 중략
    public func map<T>
// 중략
  • 전위연산자 **를 Int가 아닌 정수타입 프로토콜일 경우 연산자를 사용할 수 있도록 구현
import UIKit

prefix operator **

prefix func ** <T: BinaryInteger> (value: T) -> T {
    return value * value
}

let minusFive = -5
let five: UInt = 5

let sqrtMinusFive = **minusFive
let sqrtFive: UInt = **five

print(sqrtMinusFive) // 25
print(sqrtFive) // 25
  • 제네릭을 사용ㅇ하지 않은 swapToInts 함수
    • Int가 아닌 Double이나 String은 바꿀 수 없고 함수 다시 만들어야함
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA: Int = a
    a = b
    b = temporaryA
}

var numberOne = 5
var numberTwo = 10

swapTwoInts(&numberOne, &numberTwo)
print(numberOne) // 10
print(numberTwo) // 5
  • 함수에서 Int 대신 Any를 사용할 수 있지만, 해당 경우에는 Int는 Int끼리만 교환, String은 String끼리 교환하려고 할 때, 제한을 할 수 없음
    • 추가로, Any타입만 전달할 수 있고, String값을 그대로 전달할 수 없음

제네릭 함수

// 제네릭을 사용한 swapTwoValues 함수
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}
  • 제네릭 함수는 실제 타입 이름(Double, Any 등)을 써주는 대신 Placeholder(위 함수에서는 T)를 사용함
  • 플레이스홀더는 타입의 종류를 알려주지 않고, 어떤 타입이라는 것을 알려줌
  • 두 매개변수는 둘다 T이기 때문에, 같은 타입
  • T의 실제 타입은, 함수가 호출되는 순간에 결정됨. Int 타입의 변수가 전달되면 T는 Int가 되고, Double 타입의 변수가 전달되면 Double이 됨
  • 제네릭 함수의 플레이스 홀더를 지정하는 방법은 함수 이름 오른쪽에 <>기호를 사용하고 기호 안쪽에 플레이스 홀더 이름들을 나열하는 것
  • 하나의 타입 매개변수가 아니라 여러개의 타입 매개변수를 갖고 싶다면 <T, U, V>처럼 사용할 수 있음

제네릭 타입

// 제네릭을 사용하지 않은 InStack 구조체

struct IntStack {
    var items = [Int]()

    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }
}

var intStack: IntStack = IntStack()

// 제네릭을 사용한 InStack 구조체
struct Stack<Element> {
    var items = [Element]()

    mutating func push(_ item: Element) {
        items.append(item)
    }
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

var stack: Stack<Int> = Stack<Int>()
stack.push(1)

var stringStack: Stack<String> = Stack<String>()
stringStack.push("1")
  • Stack 구조체는 IntStack와 달리 Element 타입 매개변수를 사용했음
  • 구조체를 선언할 때, 어떤 타입을 사용할 지 명시해주는 방법은 Stack처럼 선언해주면 됨

제네릭 타입 확장

  • 제네릭을 사용하는 타입에 익스텐션으로 기능을 추가한다면, 타입 매개변수를 명시하지 않아야함
  • 대신 원래의 재네릭 정의에 명시한 타입 매개변수를 사용
// Stack 구조체의 익스텐션
extension Stack {
    var topElement: Element? {
        return self.items.last
    }
}

타입 제약

  • 제네릭 함수가 처리해야할 기능이 특정 타입에 한정되어야 처리할 수 있는 경우, 특정 프로토콜을 따르는 타입에 한정되어야 하는 경우가 있음
  • 타입 제약을 통해서 제약사항을 지정할 수 있음
  • 타입 매개변수가 특정 클래스를 상속받은 타입이나 특정 프로토콜을 준수하는 타입이어야 한다는 조건 등의 제약을 지정할 수 있음
  • 클래스와 프로토콜만 제약으로 지정할 수 있고, 열거형이나 구조체는 불가능함
// 제네릭 타입 제약의 예시
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) {
    // 함수 구현
}

struct Stack<Element: Hashable> {
    // 구조체 구현
}
  • 하나의 제약이 아니라 여러 제약을 추가하고 싶다면 where 절을 사용해서 제약을 추가 할 수 있음
// 여러 제약을 지정한 메서드
func swapTwoValues<T: BinaryInteger>(_ a: inout T, _ b: inout T) where T: FloatingPoint {
    // 함수 구현
}
  • 위 메서드는 두 프로토콜을 준수하는 타입이 존재하지 않음, 함수를 중복으로 정의하거나 새로운 타입을 정의해서 사용해야함
  • 타입 매개변수가 여러개라면, 타입 매개변수마다 제약 조건을 다르게 지정할 수 있음
댓글