본문 바로가기

iOS/Swift

Swift) 제네릭(Generic) 정복하기

 

 

 

안녕하세요 :) 소들입니다!

오늘의 두 번째 포스팅은 Generic에 대한 것입니다!

범용 타입이라구 하죠!!

 

어렵지 않은 문법이라 호딱 끝내 봅시다 :))

모든 포스팅은 편의 말투로 합니다~!!

 

 

 

 

1. Generic이란?

 

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

제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 있다

 

애플 말에 따르면 Swift에서 가장 강력한 기능 중 하나로

Swift 표준 라이브러리의 대다수는 제네릭으로 선언되어 있다고 함 :)

좀이따 보겠지만, 우리가 흔하게 사용하는 Array와 Dictoinary 또한 제네릭 타입임!!

 

아니 그래서 제네릭이 도대체 뭔데

 

 

 

1-1. 제네릭 함수(Generic Function) 

 

자, 우리가 만약 인자로 오는 두 Int 타입의 값을 swap하는 함수를 만들고 싶음

이것을 구현 해보라 하면

 

 

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

 

 

이렇게 하면 잘 구현한 것 같음

근데 위 같은 경우엔 파라미터 모두 Int형일 경우엔 문제 없이 돌아감

근데 이 만약 파라미터 타입이 Double, String일 경우엔 사용할 수 없음

 

왜냐? Swift는 타입에 민감한 언어이니까!

따라서 만약 DOuble, String에 대해서 swap 함수를 사용하고 싶다면

 

 

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)를 선언해주면,

그 뒤로 해당 이름(T)를 타입처럼 사용할 수 있음!!!

 

여기서 이 TType Parameter라고 부르는데, 

T라는 새로운 형식이 생성되는 것이 아니라,

실제 함수가 호출될 때 해당 매개변수의 타입으로 대체되는 Placeholder임!!!!

 

왜 함수 이름 뒤에 꺽쇠(<>)로 T를 감싸냐면,

위에서 말했듯 T는 새로운 형식이 아니라 Placeholder이기 때문에, 

Swift한테 T는 새로운 타입이 아니야! 그러니까 실제 이 타입이 존재하는지 찾지 마! 자리 타입이야!!

라고 말해주기 위한 것임

 

따라서 이렇게 swapTwoValues라는 함수를 제네릭으로 선언 해주면,

 

 

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

 

 

이렇게 실제 함수를 호출할 때,

Type Parameter인 T의 타입이 결정되는 것 :)

 

아, 근데 여기서 파라미터 a, b 모두 같은 타입 파라미터인 T로 선언되어 있기 때문에,

 

 

swapTwoValues(&someInt, &aotherString)       // Cannot convert value of type 'String' to expected argument type 'Int'

 

 

만약 서로 다른 타입을 파라미터로 전달하면,

첫 번째 someInt를 통해 타입파라미터 T가 Int로 결정됐기 때문에,

두 번째 파라미터인 therStrings의 타입이 Int가 아니라며 에러가 나는 것 :)

 

똑같은 내용의 함수를 오버로딩 할 필요 없이 제네릭을 사용하면 된다!!

따라서 코드 중복을 피하고 유연하게 코드를 짤 수 있다!! 

 

 

아, 그리고 타입 파라미터는 굳이 T가 원하는 이름 마음대로 해도 되고,

또 한개 말고 여러 개를 comma(,)를 이용해서 선언할 수도 있음

 

 

func swapTwoValues<One, Two> { ... }

 

 

근데 타입 파라미터 이름을 선언할 땐 보통 가독성을 위해

TV 같은 단일 문자, 혹은 Upper Camel Case를 사용한다고 합니다 :)

 

 

 

1-2. 제네릭 타입(Generic Type)

 

위에서 공부한 것처럼 제네릭을 이용한 함수를 "제네릭 함수(Generic Function)"이라고 하는데,

이 제네릭은 함수에만 가능한 것이 아니라,

구조체, 클래스, 열거형 타입에도 선언할 수 있는데, 이것을 "제네릭 타입(Generic Type)" 이라고 함

만약 Stack을 제네릭으로 만들고 싶다면 

 

 

struct Stack<T> {
    let items: [T= []
 
    mutating func push(_ item: T) { ... }
    mutating func pop() -> T { ... }
}
 

 

 

이렇게 제네릭 타입으로 Stack을 선언할 수 있단 것 :)

(클래스, 열거형 또한 가능!)

 

그럼 제네릭 타입의 인스턴를 생성할 땐 어떻게 해야할까?

 

 

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

 

 

이렇게 제네릭 타입을 선언할 땐,

선언과 마찬가지로 <>를 통해 어떤 타입으로 사용할 것인지를 명시해주어야 함 :)

근데 요거요거 어디서 많이 본 타입 선언 아님?

 

 

let array1: Array<Int> = .init()
let array2 = Array<Int>.init()

 

 

배열 생성할 때랑 똑같네!! (타입 추론 말구)

왜냐?? Swift에선 Array가 바로바로바로바로

 

 

 

 

제네릭 타입이기 때문 :)

사실 제네릭 타입을 몰라도 여러분은 아무렇지 않게 제네릭을 사용했던 것임돠

 

 

 

 

2. 타입 제약(Type Constraints) 

 

제네릭 함수와 타입을 사용할 때

특정 클래스의 하위 클래스나, 특정 프로토콜을 준수하는 타입만 받을 수 있게 제약을 둘 수 있다

 

 

 

2-1. 프로토콜 제약

 

자, 만약에 우리가 파라미터로 두 개의 값을 받아서

두 값이 같으면 true, 다르면 false를 반환하는 함수를 제네릭으로 선언하려고 함!

 

 

func isSameValues<T>(_ a: T, _ b: T) -> Bool {
    return a == b               // Binary operator '==' cannot be applied to two 'T' operands
}
 

 

 

이렇게 선언하면 될 것 같지만, 실제론 에러가 남! 왜냐면,

== 이란 연산자는, a와 b의 타입이 Equatable이란 프로토콜을 준수할 때만 사용할 수 있음

 

근데 우리가 T라고 선언한 타입 파라미터는,

a, b가 Equatable 프로토콜을 준수하는 타입일 수도, 아닐 수도 있는데

만약 아니면 어떡할라고 == 를 쓰냐! 안 된다! 라고 에러를 내는 것임!

 

따라서, 이땐 

 

 

func isSameValues<T: Equatable>(_ a: T, _ b: T) -> Bool {
    return a == b               
}
 

 

 

이렇게 타입 파라미터에 T: Equatable 이런 식으로 제약을 줄  수 있음

이렇게 하면, isSameValues란 함수에 들어올 수 있는 파라미터는

Equatable이란 프로토콜을 준수하는 파라미터만 받을 수 있음!!

 

 

 

2-2. 클래스 제약

 

클래스 제약 같은 경우엔, 프로토콜 제약과 똑같지만,

해당 자리에 프로토콜이 아닌 클래스 이름이 온느 것임!!

 

 

class Bird { }
class Human { }
class TeacherHuman { }
 
func printName<T: Human>(_ a: T) { }

 

 

이렇게 T: Human 이런 식으로 클래스 이름을 써주면,

 

 

let bird = Bird.init()
let human = Human.init()
let teacher = Teacher.init()
 
printName(bird)                  // Global function 'printName' requires that 'Bird' inherit from 'Human'
printName(human)
printName(teacher)
 

 

 

Human 클래스 인스턴스인 human과,

Human 클래스를 상속 받은(서브클래스) teacher은

printName이란 제네릭 함수를 실행시킬 수 있지만,

Human 클래스의 서브 클래슥 아닌 bird 인스턴스는 실행할 수 없음!

 

이런 식으로 클래스로 제약을 주는 것도 가능함 :))

 

 

 

 

3. 제네릭 확장하기

 

만약 제네릭 타입인 Array를 내가 확장하고 싶다면 어떻ㄱㅔ 해야 할까?

 

 

extension Array {
    mutating func pop() -> Element {
        return self.removeLast()
    }
}
 

 

 

만약 제네릭 타입을 확장하면서 타입 파라미터를 사용할 경우,

실제 Array 구현부에서 타입 파라미터가 Element이기 때문에 Element로 사용해야 함

 

만약 확장에서 새로운 제네릭을 선언하거나, 다른 타입 파라미터를 사용하면

 

 

 

 

안대영

 

 

where을 통해 확장 또한 제약을 줄 수 있는데,

 

 

extension Array where Element: FixedWidthInteger {
    mutating func pop() -> Element { return self.removeLast() }
}
 

 

 

이렇게, 타입 파라미터 Element가

FixedWidthInteger라는 프로토콜을 준수해야 한다! 라는 제약을 주면,

 

 

let nums = [123]
let strs = ["a""b""c"]
 
nums.pop()              // O
strs.pop()              // X

 

 

FixedWidthInteger 프로토콜을 준수하는 Array<Int> 형인 nums는

extension에서 구현된 pop이란 메서드를 사용할 수 있지만,

 

FixedWidthInteger 프로토콜을 준수하지 않는 Array<String> 형인 strs는

extension에서 구현된 pop이란 메서드를 사용할 수 엄따

 

 

 

 

4. 제네릭 함수와 오버로딩

 

젠네릭은 보통 타입ㅇㅔ 관계없이 동일하게 실행 되지만,

만약 특정 타입일 경우, 제네릭 말고 다른 함수로 구현하고 싶다면

이땐 제네릭 함수를 오버로딩 하면 됨

 

 

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를 실행할 경우, 제네릭 함수가 실행 됨 :)

 

 

 

 

 

 

 

 

.

.

.

 

오늘은 제네릭 공부 끝 :)

Protocol에서 제네릭을 사용하려면 associatedtype이란 것을 사용해야 하는데,

이건 나중에 프로토콜 포스팅 할 때 하겠습니다!

 

궁금점, 틀린점 지적은 언제나 댓글 주세요!!

 

 



Calendar
«   2024/04   »
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
최근 댓글
Visits
Today
Yesterday