안녕하세요 :) 소들입니다!
오늘의 두 번째 포스팅은 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)를 타입처럼 사용할 수 있음!!!
여기서 이 T를 Type 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> { ... }
|
근데 타입 파라미터 이름을 선언할 땐 보통 가독성을 위해
T나 V 같은 단일 문자, 혹은 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 Teacher: Human { }
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 = [1, 2, 3]
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이란 것을 사용해야 하는데,
이건 나중에 프로토콜 포스팅 할 때 하겠습니다!
궁금점, 틀린점 지적은 언제나 댓글 주세요!!
'iOS > Swift' 카테고리의 다른 글
Swift) Static Dispatch & Dynamic Dispatch (1/2) (9) | 2021.10.08 |
---|---|
Swift) Lottie를 이용해 애니메이션을 그려보자 (13) | 2021.08.17 |
Swift) Optional은 어떻게 구현되어 있을까? (+ Optional Pattern) (4) | 2021.04.22 |
Swift) 오버로딩(Overloading) vs 오버라이딩(Overriding) (2) | 2021.03.29 |
Swift) Any와 AnyObject 알아보기 (6) | 2021.03.23 |