[Swift] Generic


Generic


제네릭

  • 제네릭이 없다면 단순히 타입만 다르고 구현 내용이 같은 함수(클래스, 구조체, 열거형)마다 모든 경우를 다 정의해야하기 때문에 개발자의 할 일이 늘어남
  • 제네릭을 통해 타입(형식)에 관계없이 한번의 구현으로 모든 타입을 처리하여 유연함(유지보수 쉽고, 재사용성 높은) 함수 / 구조체 / 클래스 / 열거형 등을 일반화 가능한 코드로 작성 가능
// Double을 스왑하는 함수의 정의
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
    let tempA = a
    a = b
    b = tempA
}

// 문자열을 스왑하는 함수의 정의
func swapTwoStrings(_ a: inout String, _ b: inout String) {
    let tempA = a
    a = b
    b = tempA
}

// 파라미터의 타입에 구애받지 않는 일반적인(제네릭) 타입을 정의
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let tempA = a
    a = b
    b = tempA
}
  • 타입 파라미터는 함수 내부에서 파라미터 형식이나 리턴형으로 사용됨
    (함수 바디에서 사용하는 것도 가능)
  • 보통은 T를 사용하지만 다른 이름을 사용하는 것도 문제가 없음, 형식이름이기 때문에 UpperCamelcase로 선언
  • 2개이상을 선언하는 것도 가능
    (ex. <T, U>, <A, B>)
  • 제네릭은 타입에 관계없이, 하나의 정의(구현)로 모든 타입(자료형)을 처리할 수 있는 문법
  • 일반 함수와 비교해보면, 작성해야하는 코드의 양이 비약적으로 감소
  • 타입 파라미터는 실제 자료형으로 대체되는 플레이스 홀더 같은것
    (새로운 형식이 생성되는 것이 아님)
    • 코드가 실행될때 문맥에 따라서 실제 형식으로 대체되는 “플레이스 홀더”일뿐
    • 관습적으로 Type의 의미인 대문자 T를 사용하지만, 다른 문자를 사용해도 됨

제네릭 문법

함수

func printArray<T>(array:[T]) {
    for number in array {
        print(element)
    }
}

타입 파라미터의 지정

  • 함수의 이름 마지막에 꺽쇠 괄호(<T>)를 쓰고, 안에 파라미터 작성
    • 타입 파라미터는 대문자로 시작하면 상관 없음
    • 함수 내부에서 파라미터 형식이나 리턴형, 내부 변수 타입으로 사용
    • 실제 타입 대신에 사용하는 플레이스 홀더 역할
      (어떤 타입이 입력되어야 한다는 것을 제시)

타입 파라미터의 사용

  • 본래 타입의 사용하는 위치(파라미터, 바디, 리턴형)에서 타입이 필요한 곳에 타입 파라미터 사용
    • 실제 함수 호출시에 실제 타입으로 치환

클래스, 구조체, 열거형

// 클래스
class SomeClass<T> {
    var x:T
    var y:T
       
    init(x: T, y: T){
        self.x = x
        self.y = y
    }
}

// 구조체
struct SomeStruct<T> {
    var members: [T] = []
}

// 열거형
enum SomeEnumeration<T> {
    case x
    case y
    case z(T)
}

타입 파라미터의 지정

  • 타입 이름 마지막에 꺽쇠 괄호(<T>)를 쓰고 안에 타입 파라미터 작성
    • 타입 파라미터는 대문자로 시작하면 상관 없음
    • 타입 내부 속성의 타입, 메서드의 파라미터, 리턴형으로 사용
    • 실제 타입 대신에 사용하는 플레이스 홀더 역할
      (어떤 타입이 입력되어야 한다는 것을 제시)

타입 파라미터의 사용

  • 본래 타입의 사용하는 위치(속성, 메서드)에서 타입이 필요한 곳에 타입 파라미터 사용
    • 실제 함수 호출시에 실제 타입으로 치환

열거형에서 제네릭 사용 시

  • 열거형에서 연관값을 가질때만 제네릭으로 정의가능
    (어짜피 케이스는 자체가 선택항목 중에 하나일뿐(특별타입)이고, 그것을 타입으로 정의할 일은 없음)

확장

extension SomeColor {
    func getColor() -> T {
        // ...
    }
}

// where절을 통한 타입에 대한 제네릭 제약
// Int 타입에만 적용되는 확장과 getIntArray() 메서드
extension Coordinates where T == Int {
    // 튜플로 리턴하는 메서드
    func getIntArray() -> [T] {
        return [x, y]
    }
}

제네릭 타입 확장 시

  • 타입 파라미터 명사없이 확장 (SomeColor<T> -> X)
  • 본체의 제네릭에서 정의한 타입 파라미터 사용 가능

제네릭의 타입 제약

제네릭에서 모든 타입이 다 가능하지는 않도록 타입을 제약 가능

func doSomething<T:제약조건>(a:T) {
    // ...
}

프로토콜 제약 (<T: Equatable>)

  • 특정 프로토콜을 따르는 타입만 가능하도록 제약

클래스 타입 제약 (<T: SomeClass>)

  • 특정 클래스와 상속관계 내에 속하는 클래스 타입만 가능하도록 제약

프로토콜에서 제네릭 문법의 사용

프로토콜에서는 연관 타입이라는 것을 사용해 제네릭과 동일한 타입 파라미터를 지정

  • 연관 타입 (Assiciated Types)으로 선언
  • 프로토콜은 타입들이 채택할 수 있는 한차원 높은 단계에서 요구사항만을 선언하는 개념이기 때문에 제네릭 타입과 조금 다른 개념(연관 타입)을 추가적으로 도입한 것 뿐
protocol RemoteControl {  // <T>의 방식이 아님
    associatedtype Element
    // 연관형식은 대문자로 시작해야함 (UpperCamelcase)
    // 관습적으로 Element를 많이 사용
    func changeChannel(to: Element)  
    func alert() -> Element?
}

// 연관형식이 선언된 프로토콜을 채용한 타입은, typealias로 실제 형식을 표시해야함 (생략도 가능)
struct TV: RemoteControl {
    
    typealias Element = Int  // 생략 가능
    
    func changeChannel(to: Int) {
        print("TV 채널바꿈: \(to)")
    }
    
    func alert() -> Int? {
        return 1
    }
}

class Aircon: RemoteControl {

    // typealias를 생략해도 연관 형식이 추론됨
    
    func changeChannel(to: String) {
        print("Aircon 온도바꿈: \(to)")
    }

    func alert() -> String? {
        return "1"
    }
}

// 프로토콜에서 타입을 제약하는 경우
protocol RemoteControl2 {
    associatedtype Element: Equatable
    // <T: Equatable> 방식으로 제약조건 추가
    func changeChannel(to: Element)
    func alert() -> Element?
}

참고

Ellen Swift Class






© 2021. by hminkim