본문 바로가기

 𝗔𝗣𝗣𝗟𝗘/SWIFT : GRAMMAR

Swift 기초 문법 - 제네릭(Generic)

 

 

 

 

제네릭(Generic)

 

 

 

 

 

💡 제네릭(Generic)

- 제네릭이란 타입에 의존하지 않는 범용 코드를 작성할 때 사용

- 코드의 중복을 피하고, 유연하게 작성할 수 있음

- 실제 타입 이름을 써주는 대신에 placeholder를 사용

   → placeholder는 타입의 종류를 알려주지 않지만 어떤 타입이라는 것은 알려줌

    placeholder의 실제 타입은 함수가 호출되는 순간 결정

    placeholder는 타입 매개변수로 쓰일 수도 있음, 이 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환됨

- 구조체, 클래스, 열거형 등이 어떤 타입과도 연관되어 동작할 수 있음

- ArrayDictoinary도 제네릭 타입

 

 

 


 

 

 

제네릭 함수(Generic Function)

- 타입에 제한 두지 않고 사용하고 싶을 때 사용

 

 

예시

// Code(01)

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

 

Code(01)에 경우 파라미터 모두 Int형일 경우엔 문제 없이 돌아가나,

swift는 민감한 언어이기 때문에 만약 파라미터 타입이 Double, String일 경우 사용할 수 없음

 

 

만약 Double, String에 대해 swap함수를 사용하고 싶다면,

// Code(02)

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
}

 

Code(02) 처럼 하나하나 형식에 맞게 오버로딩 해야 사용할 수 있음 근데 귀찮잖아...

 

이럴 때 쓰는 함수가 제네릭 함수

 

 

결론은,

// Code(03)

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

 

앞서 말했듯 Double, String에 대한 swap함수 사용에 경우 Code(03) 처럼 제네릭 함수를 사용해야 하며,

사용 시 Code(04)와 같이 Type ParameterT의 타입이 결정됨

 

// Code(04)

var someInt = 1
var aotherInt = 2
swapTwoValues(&someInt,  &aotherInt)          // 함수 호출 시 T는 Int 타입으로 결정됨


var someString = "Hi"
var aotherString = "Bye"
swapTwoValues(&someString, &aotherString)     // 함수 호출 시 T는 String

 

 

 


 

 

 

제네릭 사용법

func 사용할_함수_이름<T>()

※ 간단하게 말하면 이런 식으로 정의할 수 있음

 

여기서 T란?

   - "Type Parameter"는 일종의 타입 매개변수

   - 제네릭에서 실제 타입으로 대체될 수 있는 임의의 타입을 나타냄

   - 일반적으로 제네릭 함수나 제네릭 데이터 구조를 작성할 때 사용

   - T는 새로운 형식이 아닌,

      함수나 데이터 구조가 호출될 때 실제로 대체되는 placeholder라는 것을 나타냄

 

 

제네릭 타입(Generic Type)

   - 특정한 타입이 아닌, 여러 다른 타입을 수용할 수 있는 타입

   - 함수, 메서드, 클래스, 구조체, 열거형 등을 정의할 수 있음

 

 

예시

스택(Stack)이라는 자료구조를 제네릭으로 구현한다면?

struct Stack<T> {
    let items: [T] = []

    mutating func push(_ item: T) { ... }
    mutating func pop() -> T { ... }
}

 

 

제네릭 타입의 인스턴스를 생성

let stack1: Stack<Int> = .init()
let stack2 = Stack<Int>.init()

 

※ 제네릭을 사용하지 않는다면 스택은 특정 타입(예: Int, String)의 요소만을 저장할 수 있을 것

그러나, 제네릭을 사용하여 스택을 구현한다면 Int 타입 뿐만 아니라 어떤 타입이든 요소로 저장할 수 있게 됨

 

  

 


 

 

 

💡 타입 제약(Type Constraints)

   - 제네릭 타입이나 함수가 특정 조건을 충족하는 타입만을 수용하도록 하는 기능

   - 타입 제약은 코드의 안전성을 높이고 의도하지 않은 타입이 사용되는 것을 방지하기 위해 사용

   → 이를 통해 제네릭 코드를 보다 정교하게 제어하고, 보다 안전하게 사용할 수 있음

   - 일반적으로 제네릭 코드에서는 어떤 타입이든 받아들일 수 있으나,

     만약 특정 타입에 대해 제한을 두고 싶을 때가 있을 경우

   → 이때 타입 제약을 사용하여 특정 타입의 조건을 명시할 수 있음

 

 

 


 

 

 

타입 제약(Type Constraints) 활용

특정 프로토콜을 따르는 타입만 허용

   - 제네릭 타입이나 함수가 특정 프로토콜을 준수하는 타입만을 수용하도록 할 수 있음

 

클래스 타입으로 제한

   제네릭 타입이나 함수가 클래스 타입만을 수용하도록 할 수 있음

    이는 상속이 가능한 타입에 대해서만 제네릭을 적용할 수 있도록 함

 

특정 타입으로 제한

   제네릭 타입이나 함수가 특정한 클래스나 구조체 타입을 받아들일 수 있도록 할 수 있음

 

프로토콜 제약(Protocol Constraints)

   - 제네릭 타입이나 함수가 특정 프로토콜을 준수하는 타입만을 수용하도록 하는 것을 의미

   → 이를 통해 제네릭 코드가 특정한 프로토콜을 따르는 타입에 대해서만 동작하도록 제한할 수 있음

 

 

 


 

 

 

프로토콜 제약

타입 안전성 보장

   - 프로토콜 제약을 사용하여 특정 프로토콜을 따르는 타입만을 받아들이도록 함으로써, 코드의 안전성을 보장할 수 있음

코드 재사용성

   - 특정 프로토콜을 따르는 다양한 타입에 대해 동작하는 제네릭 코드를 작성할 수 있으므로, 코드의 재사용성이 높아짐

 

 

예시

func someFunction<T: SomeProtocol>(someParameter: T) {
    // ...
}

 

T: SomeProtocol

  • 제네릭 함수 someFunction이 제네릭 타입 T를 사용할 때
  • SomeProtocol 프로토콜을 준수하는 타입에 제한을 둔다는 것을 나타냄
  • 따라서 someParameter 매개변수로 전달되는 값은 SomeProtocol 프로토콜을 준수하는 타입이어야 함

 

 


 

 

 

제네릭 확장(extension)

   - 기존의 구조체, 클래스, 열거형 등에 대한 확장에서 타입 매개변수를 사용할 수 있음

   → 이를 통해 동일한 기능을 다양한 타입에 대해 적용할 수 있음

 

 

예시

※ Array에 대한 확장 정의

printElements() : 배열의 요소를 출력하는 기능

extension Array {
    func printElements() {
        for element in self {
            print(element)
        }
    }
}

// 사용 예시
let numbers = [1, 2, 3, 4, 5]
let strings = ["apple", "banana", "orange"]

numbers.printElements() // 출력: 1 2 3 4 5
strings.printElements() // 출력: apple banana orange

 

 

 


 

 

 

 

제네릭 함수와 오버로딩

   - 특정 타입에 대해 다르게 동작하도록 제네릭 함수를 오버로딩할 수 있음

   → 이를 통해 제네릭 함수와 다른 함수 간에 우선순위를 정할 수 있고, 특정 타입에 대한 특별한 처리를 할 수 있음

   → 따라서 필요에 따라 제네릭 함수를 오버로딩하여 원하는 동작을 구현할 수 있음

func swapValues<T>(_ a: inout T, _ b: inout T) {
    print("generic func")
    let tempA = a
    a = b
    b = tempA
}

func swapValues(_ a: inout Int, _ b: inout Int) {
    print("specialized func")
    let tempA = a
    a = b
    b = tempA
}

 

위 코드경우 타입이 지정된 함수가 제네릭 함수보다 우선순위가 높음

그렇기 때문에,

 

var a = 1
var b = 2
swapValues(&a, &b)          //"specialized func"


var c = "Hi"
var d = "Sodeul!"
swapValues(&c, &d)          //"generic func"

 

Int 타입으로 swapValue를 실행할 경우 : 타입이 지정된 함수가 실행

String 타입으로 swapValue를 실행할 경우 : 제네릭 함수가 실행

Recent Posts
Visits
Today
Yesterday
Archives
Calendar
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31