본문 바로가기
iOS/Swift

[iOS/Swift] 타입에 의존하지 않는 범용코드, 제네릭(Generic)

by iosdevlime 2023. 3. 12.

SwiftUI를 다루던 도중, 

body 메서드가 채택하는 요상한 some View 이름의 타입을 발견하게 되었는데요..

 

해당 타입을 '불 투명 반환타입(Opaque return type)', 또는

'역 제너릭 타입' 이라고 불린다 카니..

 

부랴부랴 급하게 제네릭(Generic)에 대한 포스팅을 들고 왔습니다.

 

 

 


 

 

 

제네릭(Generic)이란?

특정 타입에 의존하지 않는, 유연한 코드를 위한 Swift의 강력한 도구 중 하나

 

제네릭(Generic)의 사전적 정의는 다음과 같습니다.

Generic
포괄적인, 총칭의, 이름이 붙지 않은
'일반적'이란 뜻의 'General'과 동일한 어원

 

Swift 표준 라이브러리의 대다수는 이러한 '제네릭'으로 선언되어 있으며,

Collection Type 중, ArrayDictionary 가 대표적인 예시입니다.

 

 

The Swift Language Guide에서 제네릭에 대해 잘 다뤄주고 있으니,

해당 문서를 천천히 따라가면서 살펴보도록 하겠습니다.

 

지네릭 (Generics) - The Swift Language Guide (한국어)

Dictionary의 Key, Value와 같이 엘리먼트 간의 서로 상관관계가 있는 경우 의미가 있는 이름을 파라미터 이름으로 붙이고 그렇지 않은 경우는 T, U, V와 같은 단일 문자로 파라미터 이름을 짓습니다.

jusung.gitbook.io

 

 

 

제네릭이 해결할 수 있는 문제

  • 다음 swapInts 함수는 매개변수 a와 b의 값을 전환하는 기능을 수행합니다.
    • 함수 Scope 외부에서 활용하기 위해 in-out parameters 로 작성합니다.
    • a와 b의 값은 정상적으로 Swap됩니다.
func swap(_ a: inout Int, _ b: inout Int) {

    let tempA = a // 섀도우 변수 tempA에 매개변수 a를 할당하고,
    a = b // 매개변수 a에는 매개변수 b를 할당하고,
    b = tempA // 매개변수 b는 섀도우 변수 a를 할당한다

    // 그렇게 되면, a와 b는 값이 서로 Swap 될 것임
}
var intA = 12
var intB = 20

swap(&intA, &intB) // 정상적으로 잘 작동!

 

  • 그런데, 만약 정수형(Int)이외 매개변수 타입이 문자열일 경우에는?
    • swapInts 함수의 매개변수 타입은 정수이므로, 당연히 활용할 수 없습니다.
    •  동일한 기능을 위해 중복된 함수(오버로딩)를 생성하는 것은 효율적이지 않습니다.
func swap(_ a: inout String, _ b: inout String) {
    
    let tempA = a
    a = b
    b = tempA
}

var stringA = "Lime"
var stringB = "Jason"

swap(&stringA, &stringB)

 

 

  • 이러한 경우, 활용할 수 있는 방식이 바로 '제네릭'(Generic)입니다.
    • 타입에 제한을 두지 않고, 특정 기능의 코드(함수)를 재 사용할 때 유용합니다.
    • 꺽쇠 <> 를 활용, 내부에 타입처럼 사용할 이름(T)을 작성하면, 모든 타입에서 사용 가능합니다.
      (파라미터의 이름은 T,U,V와 같은 단일문자를 작성하는 것이 규칙입니다)
func genericSwap<T> (_ a : inout T, _ b: inout T) {
    let tempA = a
    a = b
    b = tempA
}

 

  • 제네릭 타입 T는 Type Parameters라고 하며, 함수가 호출될 시 타입이 결정되는 placeholder입니다.
    • 함수 내부에서의 T는 새로운 타입이 아닌, 소위 말해 '비어있는 자리' 입니다.
    • 따라서, 해당 함수가 호출될 시점에서 T의 타입이 결정됩니다.
...

var number1 = 5
var number2 = 10

genericSwap(&number1, &number2) // 아, 제네릭(범용)타입 T는 Int타입으로 설정되었구나!

var string1 = "Lime"
var string2 = "Jason"

genericSwap(&string1, &string2) // 아, 제네릭(범용)타입 T는 String타입으로 설정되었구나!

 

 


 

 

제네릭 타입(Generic Type)

구조체, 클래스, 열거형 타입과 함께 선언할 수 있는 제네릭 타입(Generic Type)

 

앞서 살펴본 제네릭의 기능을 정리하자면 다음과 같습니다.

  • 재 사용이 가능한 범용적인 타입의 함수를 만듬
  • 비어있는 자리(Placeholder) 형태의 타입으로, 함수 호출 시 타입이 결정

 

그런데, 이러한 제네릭은 함수 이외

구조체나 클래스, 열거형 타입에서도 매우 유용하게 활용할 수 있습니다!

(이는 추후 SwiftUI 프레임워크의 View 프로토콜과도 연계되는 내용이므로 반드시 알아두어야 합니다)

 

 

 

 

제네릭 타입 선언 및 활용방법

  • 다음 예시코드의 Customer 구조체는 다음과 같은 제네릭 타입으로 선언됩니다. 
    • 제네릭 함수 선언방식과 동일하게 구조체 이름(Customer)옆꺽쇠<> 타입이 선언됩니다.
    • 또한, 내부 프로퍼티 타입이나 인스턴스 메서드의 매개변수 또한 제네릭 타입으로 선언 가능합니다.
struct Customer<T> {
    var number: [T] = []
    
    mutating func deleteNumber(_ names: T) {
    }
}

 

  • 제네릭 타입의 인스턴스의 경우, 반드시 꺽쇠<> 내부에 어떠한 타입인지 명시해야 합니다. 
    • Customer 구조체의 경우, 초기화가 되어있지 않으므로 .init() 메서드를 활용합니다.
    • number 프로퍼티의 값에 정수 배열값을 추가할 수 있습니다.
var order: Customer<Int> = .init()

order.number = [1,4,6]
order.number // [1,4,6]

 

 


 

 

타입 제약(Type Constraints)

제네릭 함수 및 타입을 활용할 시, 특정 클래스 혹은 프로토콜을 통한 타입제약

 

제네릭 타입(Generic Type)의 경우, 모든 타입에서 작동이 잘 되는 것을 확인할 수 있었습니다!

 

그런데, 특정 타입만 사용할 수 있도록 제네릭을 사용할 순 없을까요? 

 

 

 

프로토콜 제약(Protocol Constraints)

  • 다음 함수 IsEqual은 제네릭 타입으로 선언되어 있으며, 매개변수 a와 b가 존재합니다. 
    • 매개변수 a와 b가 동일할 경우 true, 그렇지 않다면 false를 반환하도록 합니다.
    • 하지만, 비교 연산자(==)를 활용할 수 없다런타임 오류가 발생합니다!
func isEqual<T>(_ a: T, _ b: T) -> Bool {
    return a == b // Binary operator '==' cannot be applied to two 'T' operands 오류 발생
}

 

  • 비교 연산자(==)는 매개변수 a와 b의 타입이 'Equtable' 프로토콜을 준수할 경우만 활용할 수 있습니다. 
    • 여기서 Equtable 프로토콜이란, 쉽게 말해 '값이 동일한지, 아닌지 비교할 수 있는' 타입입니다.
    • Swift 표준 라이브러리 내 기본 데이터 타입(Int, String, Bool..)은 해당 프로토콜을 따르고 있습니다.
    • 따라서, 아래와 같이 <T> 제네릭 타입에 Equtable 프로토콜을 작성하므로서, 일종의 '제약'을 설정합니다.
func isEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
    return a == b // Binary operator '==' cannot be applied to two 'T' operands 오류 발생
}

isEqual(5, 5) // true
isEqual(10, 1) // false

 

 

 

 

클래스 제약(Class Constraints)

  • 클래스 제약의 경우, 프로토콜과 동일하게 활용되며 해당 키워드에 클래스 이름이 자리합니다. 
    • 아래와 같이 2개 클래스(Animals, Human)와, Human 클래스를 상속받는 'Son'클래스가 있습니다.
    • 추가로 제네릭 타입이자 Human 클래스를 채택callName 함수가 존재합니다.
class Animals { }
class Human { }
class Son: Human { }

func callName<T: Human>(_  a: T) { }

 

  • 각각의 클래스의 인스턴스를 생성하고, callName 함수의 제네릭 타입 매개변수에 할당합니다. 
    • human, son 인스턴스는 Human클래스를 채택한 callName 함수에서 실행할 수 있습니다.
    • 하지만, Animals는 어떠한 관계도 갖지 못하므로, 실행할 수 없으며 제약이 설정되게 됩니다.
var animals = Animals.init()
var human = Human.init()
var son = Son.init()

callName(animals) // Global function 'callName' requires that 'Animals' inherit from 'Human'
callName(human)
callName(son)

 

 


 

재 사용이 가능하며, 범용되어 사용되는

제네릭(Generic) 함수와 타입, 제약방식에 대해 살펴보았습니다.

 

본 포스팅에서 미쳐 다루지 못했던 

연관타입(Associated Type), Where절 활용  등의 심화된 내용은

이후 추가로 알아볼 프로토콜 포스팅에서 함께 녹아내도록 하겠습니다.

 

댓글