BOID

[swift] 제네릭이란?(1/2) 본문

swift 시작기

[swift] 제네릭이란?(1/2)

HoonIOS 2021. 3. 18. 15:11

안녕하세요 HoonIOS입니다 :) 반가워요

이번에는 제네릭에 대해서 알아보겠습니다, 이 부분은 들어본 적은 있지만 개념을 잘몰라 공부를 해봤습니다. 그 내용을 한번 포스팅 해볼려고 합니다 재밌게 읽어주세요 ㅎㅎ

 

제네릭이란?

- 제네릭을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있습니다. 또 제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고 코드의 중복을 줄일 수 있기 때문에 추상적인 표현이 가능합니다.

- 실제로 제네릭은 애플 표준 라이브러리에서도 많이 사용을 합니다. 예를들어, Array, Dictionary, Set 등의 타입은 모두 제네릭 컬렉션입니다. Int, String타입을 요소로 갖는 배열을 만들거나 그 외의 어떤 타입도 배열을 요소로 가질 수 있었던 것은 모두 제네릭 덕분입니다.

- 제네릭을 사용할때는 제네릭이 필요한 타입 또는 메서드의 이름 뒤에 홀화살괄호 기호(<>) 사이에 제네릭을 위한 타입 매개변수를 써주어 제네릭을 사용할 것임을 표기하면 됩니다.

- 문법은 다음과 같습니다.

제네릭을 사용하고자 하는 타입 이름<타입 매개변수> //제네릭 타입
제네릭을 사용하고자 하는 함수 이름<타입 매개변수> (함수의 매개변수 ...) { ... } //제네릭 함수

- 그럼 예제를 통해서 제네릭을 알아보겠 습니다.

prefix func **(value: Int) -> Int {
	return value * value
}

let minusFive: Int = -5
let sqrtMinusFive: Int = **minusFive
    
func swapTwoValues( _ a: inout Int, _ b: inout Int) {
    let temporaryA: Int = a
    a = b
    b = temporaryA
}

- 위 함수는 Int 타입에서만 사용자 정의 함수를 사용할 수 있습니다, 즉 UInt 타입, 즉 부호가 없는 정수 타입에서는 Int 타입에 구현해준 사용자 정의 연산자를 사용하지 못합니다.

- 프로토콜과 제네릭이라는 스위프트의 기능을 조합하여 정수 타입 프로토콜, 즉 BinaryInteger 프로토콜에 해당하는 값이면 해당 연산자를 사용할수 있도록(제네릭을 이용하여) 구현해 줄 수 있습니다.

 

- 그러면 이제 위 코드를 프로토콜과 제네릭을 이용하여 전위 연산자를 구해보겠습니다.

prefix operator **

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

let minusFIve: Int = -5
let finve: UInt = 5

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

//둘다 print하면 25가 나옵니다

 - 우선 위 예제를 그냥 떡 하고 보면 어려울 수도 있으니깐 좀 더 쉬운 예제를 통해 확인하고 그다음 다시 한번 살펴보겠습니다.

- 위 제네릭이 왜 필요한지 설명을 했는데요, 그러면 굳이 제네릭을? Any타입을 쓰면 안 될까라는 의문이 생기실 수 있습니다. 일반 Any의 타입을 매개변수로 할당하고 swap 하는 함수를 만들면 문제점이 있습니다.

  1. Any타입의 두 변수에 어떤 타입의 값이 들어있는지 모른다, 따라서 같은 타입끼리 swap을 교환할 수 있게 제약을 둘 수 없습니다.
  2. Any타입의 inout 매개변수를 통해 전달된 전달 인자의 타입은 Any로 전달돼야 합니다. 다른 타입의 변수로 전달 인자를 전달받을 수 없습니다.
    예를 들어, 참조 타입이므로 A = C, B = D를 하고 swap을 하면 A와 B를 swap함수에 대입하면 클래스는 참조 타입이므로 A와 B만 swap 되고 C와 D의 값은 바뀌지 않습니다. 

제네릭 함수란?

- 제네릭 함수는 위에서 말한 문제점을 해결할 수 있습니다. 즉 같은 타입의 두 변수의 값을 교환한다는 목적을 타입에 상관없이 할수 있도록 단, 하나의 함수를 구현할 수 있습니다

 

- 제네릭으로 사용한 swap 함수를 예제로 설명해보겠습니다.

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

- 위 예제를 보면은 제네릭 함수는 실제 타입 이름인 Int, String 등을 써주는 대신에 플레이스 홀더(위 예제에서는 T)를 사용합니다. 플레이스 홀더(T)는 타입의 종류를 알려주지 않지만 말 그대로 어떤 타입을 사용한다는 것을 알려주는 역할을 합니다.

- 즉 함수의 매개변수 a와 b에 같은 플레이스 홀더를 선언해주었으므로 두 매개변수는 같은 타입이라는 거는 알 수 있습니다.

- 제네릭 함수의 플레이스 홀더를 지정하는 방법은 함수 이름 오른쪽의 홀 화살 괄호 기호 (<>)안 쪽에 플레이스 홀더 이름을 나열하면 됩니다.

- 위 예제에서는 func swapTwoValues<T>에서는 T가 이 함수의 플레이스 홀더로 사용된다는 것을 뜻한다.

- 그럼 T는 언제 정해 질까요? 

T의 실제 타입은 함수가 호출되면서 결정하게 됩니다.

- 타입 매개변수를 지정해주면 이를 함수의 매개변수 타입으로 사용할 수 있습니다. 또는 함수의 반환 타입으로도 사용을 할 수 있으며, 함수 내부 변수의 타입 지정을 위해 사용할 수도 있습니다.
이렇게 각각의 경우 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환이 됩니다.

- 하나의 타입 매개변수를 갖지 않고 만약 여러 개의 타입 매개변수를 가지고 싶다면 홀화살 괄호 기호 안쪽에 쉼표로 분리한 여러개의 타입 매개변수를 지정해줄 수 있습니다.

예를 들어, <T, U, V>처럼 지정해주면 됩니다.

 

- 좀 더 꿀팁을 이용하자면 의미 있는 이름을 타입 매개변수의 이름을 지정해 주면 제네릭 타입 및 제네릭 함수와 타입 매개변수와의 관계를 조금 더 명확히 표현해 줄 수 있습니다.

예를 들어, 실제로 쓰이는 라이브러리에서 보자면 Dictionary<Key, Value>처럼 하면 됩니다.

제네릭 타입

-제네릭 타입을 구현하면 사용자 정의 타입인 구조체, 클래스, 열거형 등 어떤 타입과도 연관되어 동작을 할 수 있습니다. Array, Dictionary 타입이 자신의 요소로 모든 타입을 대상으로 동작할 수 있다는것과 유사합니다.

- 제네릭 타입을 이용하여 이제 모든 타입을 대상으로 동작할 수 있는 스택기능을 예로들어 구현해보겠습니다.

단, 모든 타입을 대상으로 동작할수 있다는 게 타입이 여러 개로 섞여 들어오는 게 아니다. 만약 모든 타입을 수용할 수 있도록 구현하려면  제네릭 타입을 Any로 타입을 지정해 주면 됩니다.

- 이번 예제에서는 스택의 요소로 한 타입을 지정해주면 그타입으로 계속 스택이 동작하길 빌고, 처음 지정해주는 타입은 스택을 사용하고자 하는 사람 마음대로 지정할 수 있도록 설정한 제네릭이다.

- 제네릭을 이용한 Stack구현

struct Stack<Element> {
	var items = [Elemnet]()
    //배열에 append하기
    mutating func push(_ item: Element) {
    	items.append(item)
    }
    //배열에서 remove하기
    mutating func pop() -> Element {
    	return items.removeLast()
    }
}

- 위 Stack의 인스턴스를 생성할 때 실제로 Element 대신 어떤 타입을 사용할지 명시해주는 방법은 Stack <Type>처럼 선언해주면 됩니다.

- 만약 모든 타입을 다 수용하게 하고 싶다면 var anyStack: Stack <Any> = Stack <Any>()로 Any타입으로 정의하면 훨씬 광범위하게 사용할 수 있습니다.

제네릭 타입 확장

-만약 익스텐션(extension)을 통해 제네릭을 사용하는 타입에 기능을 추가하고자 한다면 익스텐션 정의에 타입 매개변수를 명시하면 안 됩니다. 대신 원래인 제네릭 정의에 명시한 타입 매개변수를 익스텐션으로 사용할 수 있습니다.

 

- 위 제네닉을 이용한 Stack 구현한 것에서 extension을 해보겠습니다.

extension Stack {
	var topElement: Element? {
    	return self.items.last
    }
}

- 위 extension예시처럼 익스텐션에 따로 타입 매개변수를 지정해주지는 않았지만 기존의 제네릭 타입인 Element 타입을 사용할 수 있습니다.

 

혹시 extension이 뭔지 모르시면 여기 extension(확장)에 대해 포스팅한 걸 읽어보시는 걸 추천드립니다!

 

지금까지 제네릭에 대해 알아봤는데요 내용이 너무 길어서 1, 2로 나누게 되었습니다....ㅠㅠ 다음 제네릭의 타입 제약에 대해 설명을 이어가겠습니다.! ( 링크 걸어두었습니다!)

 

좋은 하루 보내세요 감사합니다 :)

반응형
Comments