[Swift] 제네릭 (Generic)
제네릭을 사용하면 모든 타입에서 작동할 수 있는 유연하고 재사용가능한 함수 및 타입을 정의할 수 있음
Array
, Dictionary
타입은 모두 제네릭한 콜렉션타입이다.
배열은 [Int],[String] 등 타입이 다른 배열을 생성 가능하다. 따라서 배열 또한 제네릭 타입임
제네릭이 해결하는 문제 (The Problem That Generics Solve)
다음 예를 보자
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
성공적으로 변경 된다.
문제는 여기서 일어난다
만약 변경하려는 타입이 Int
가 아닌 Double
, String
이라면
같은 함수를 타입만 바꿔 추가로 선언해줘야 한다
func swapTwoDoubles(_ a: inout Double, _ b: inout Double)
func swapTwoStrings(_ a: inout String, _ b: inout String)
이러한 문제를 제네릭을 사용하면 모든 타입의 두 값을 교환할 수 있는 유연한 하나의 함수를 작성할 수 있다.
제네릭 함수 ( Generic Functions)
위의 함수를 제네릭 버전으로 사용해보자
func swapTwoValues<T>(_ a: inout T, _b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
함수의 제네릭 버전은 실제 타입의 이름 대신 자리 표시자 이름(여기서는 T
를 사용함)을 사용한다
자리표시자 이름은 T
가 무엇인지 말해주지 않지만 무엇이든 간에 a
와 b
는 T
타입으로 말한다
T
에 사용될 실제 타입은 이 함수가 실행될 때 결정된다
자리표시자는 함수의 이름 뒤 꺽쇠<>
를 감싼 부분에 들어가며 이를 Swift에게 알려준다
따라서 Swift는 자리표시자 이름에 사용된 이름의 실제 타입을 찾지 않는다
이 함수는 이제 두 값이 같은 타입인 한 모든 타입의 두 값을 교환할 수 있는 유연한 함수가 된다.
T에 사용할 타입은 함수에 전달된 인수의 값 타입으로 추론하여 결정된다
var a = "aaa"
var b = "bbb"
swapTwoValues(&a, &b)
print("\(a) \(b)")
var doubleA = 1.0
var doubleB = 2.0
swapTwoValues(&doubleA, &doubleB)
print("\(doubleA) \(doubleB)")
인수에 a와 b를 전달 할 때는 T
는 String
타입으로 결정되고
인수 doubleA와 doubleB를 전달할 때는 T
가 Double
타입으로 결정 된다
타입 파라미터 ( Type Parameter )
자리표시자
T는 타입 파라미터의 예이다
타입 파라미터는 자리표시자 타입을 지정하고 이름을 정한다
함수명 바로 뒤에 꺾쇠 사이에 작성된다 ex) <T>
타입 파라미터
를 지정하면 함수 매개변수의 타입을 정의하고
함수의 반환 타입, 함수 내에서 타입으로 사용할 수 있다
func genericMethod<T>(_ a: T, b: T) -> T {
var sum: T = a + b
return sum
}
genericMethod
함수는 타입 파라미터
<T>
를 정의
genericMethod
함수는 T타입의 매개변수 a
, b
를 입력 받아 T
타입을 반환 하는 함수
함수 내에서 변수 sum
을 T
로 정의
꺾쇠 괄호 안에 쉼표(, )
로 구분하여 둘 이상의 타입 파라미터를 제공할 수 있음
func someGenericMethod<T, V>(_ a: T, b: V)
Generic Types
제네릭 함수
외에 제네릭 타입
을 정의할 수 있음
Array
, Dictionary
와 유사한 방식으로 모든 타입에서 사용할 수 있는
사용자 지정 Class
, Struct
, Enum
을 제네릭 타입으로 사용할 수 있다
아래는 일반적인 Int 타입의 값을 가지는 스택을 구현한 코드이다
struct IntStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
items.removeLast()
}
}
IntStack
은 구조체로서
Int
타입의 값을 가지는 [Int]
타입의 프로퍼티 items
를 가지고 있고
push
, pop
메서드를 이용하여 items
의 값을 수정함.
items
의 값을 수정해야 하기 때문에 mutating
키워드를 사용
이 코드는 잘동작하지만 Int
타입의 값에서만 동작한다.
이를 보다 유연한 제네릭 코드로 바꿔보자
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
items.removeLast()
}
}
타입 파라미터
<Element>
를 정의 하였고
각 프로퍼티, 메서드의 매개변수, 메서드의 반환 타입에 타입 파라미터
Element
를 사용하여
유연한 제네릭 타입의 Stack 자료구조가 되었다.
여기서 Element
는 다음 위치에서 자리 표시자
로 사용이 됨
Element
타입의 비어 있는 배열로 초기화 되는items
프로퍼티를 생성push(_:)
메서드에서Element
타입의 매개변수item
pop()
메서드에서 반환되는Element
타입의 값
이 스택은 제네릭 타입이기 때문에 Array
, Dictionary
처럼 Swift
에서 모든 유효한 타입의
스택을 생성할 수 있음
인스턴스 생성은 items
에 저장할 타입을 꺾쇠 안에 작성하여 생성
var mStack = Stack<Double>()
mStack.push(5.0)
mStack.push(6.0)
mStack.push(7.0)
print(mStack.pop()
제네릭 타입의 확장 (Extending a Generic Type)
제네릭 타입을 확장할 때는 확장의 정의에 타입 파라미터를 작성하지 않아도 됨
원래 정의부에 있는 타입 파라미터명을 확장 내에서 그대로 사용이 가능함
// 제네릭 타입을 확장할 땐 타입 파라미터를 명시하지 않아도 됨
extension Stack {
// 확장 안에서 타입 파라미터를 자리표시자로 사용 가능
var topItem: Element? {
items.isEmpty ? nil : items[items.count - 1]
}
}
제네릭 타입의 정의부에서 정의한 타입 파라미터 Element
를
확장 안에서 자리표시자로 사용할 수 있음
제네릭 타입의 확장에는 추후 설명할 제네릭 Where절과 확장에서 새로운 함수를 사용하기 위해 확장 타입의 인스턴스가 충족해야 하는 요구 사항도 포함할 수 있습니다
타입 제약 조건(Type Constrains)
타입 제약 조건은 타입 파라미터가 다음과 같아야 함을 요구 하는 것
- 특정 클래스를 상속
- 특정 프로토콜을 준수
- 특정 프로토콜 타입
예를 들어 Dictionary
타입의 Key
로 사용할 수 있는 타입은 Hashable
가능해야 함
@frozen public struct Dictionary<Key, Value> where Key : Hashable
따라서, Hashable
프로토콜을 준수해야 Dictionary
의 Key
로 사용할 수 있음을 제약조건으로 지정
이처럼 사용자 지정 제네릭 타입을 만들때, 고유한 타입 제약 조건을 정의할 수 있으며
이러한 제약 조건은 제네릭 프로그래밍에 많은 기능을 제공한다
모든 Swift의 기본 타입 ( Int, Double, String, Bool 등)은 기본적으로 Hashable 함
타입 제약 조건 구문 (Type Constraint Syntax)
타입 파라미터 뒤에 :
(콜론) 으로 구분된 class
, protocol
과 같은 제한할 타입을 지정하여 정의
제네릭 함수의 제약 조건에 대한 기본 구문은 다음과 같다
(제네릭 타입도 동일하다)
// T는 SomeClass를 상속 받는 타입이어야함
// U는 SomeProtocol을 준수하거나 상속받는 타입이어야 함
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {}
struct SomeStruct<T: SomeStruct, U: SomeProtocol> { }
타입 제약조건의 사용 (Type Constraint in Action)
제네릭 함수가 아닌 일반 함수 findIndex(ofString:in:)
함수를 보자
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind { return index }
}
return nil
}
이 함수는 배열 array
에서 발견된 첫 일치하는 문자열의 인덱스를 반환하고 찾지 못한다면 nil
을 반환함
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// The index of llama is 2
이 함수를 제네릭 함수로 변경해보자
func findIndex<T>(of valueToFind: T, in array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind { return index }
}
return nil
}
타입 파라미터
T
를 선언하고 각 인자 레이블의 타입을 T
, [T]
로 정의 하였을 뿐 이전 함수와 똑같음.
실행하면 컴파일 되지 않는다.
Binary operator ‘==’ cannot be applied to two ‘T’ operands
value
, valueToFind
의 타입은 모두 T
이다.
T
타입은 연산자 ==
을 사용할 수 없다라는 에러메세지이다.
Swift에서는 모든 타입이 같음 이라는 == 연산자를 사용하여 비교할 수 있지 않다
예를 들어
struct SomeStruct {}
let structType = SomeStruct()
let structType2 = SomeStruct()
if structType == structType2 { ... }
structType
와 structType2
를 ==
연산자로 비교할 수 있지 않음을 알 수 있다
이 때문에 모든 타입을 대변하는 T
가 ==
연산자를 사용할 수 있음을 보장할 수 없으며,
이러한 코드를 컴파일 할 때 컴파일 에러가 발생된다.
그렇다면 ==
연산자를 사용하는 타입들은 뭐가 다른걸까?
Swift 표준 라이브러리는 해당하는 타입의 두 값을 비교하기 위해 ==
연산자와 !=
연산자를 구현하여
비교할 수 있게 하는 Equatable
프로토콜
을 가지고 있음.
Equatable
프로토콜
을 준수하는 타입들만이 같음 혹은 같지 않음을 비교할 수 있으며
Swift의 표준 타입들은 Equatable
프로토콜
을 자동으로 지원한다
findIndex<T>(of:in:)
함수에서 문제가 되는 부분은 T
타입끼리의 비교이다.
따라서 T
타입은 Equatable
프로토콜
을 지원하는 타입이라고 제약조건을 명시하면 등호 연산자를 지원하는 타입임을 Swift가 알고 있기 때문에 안전하게 사용이 가능하다
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int?{
for (index, value) in array.enumerated() {
if value == valueToFind { return index }
}
return nil
}
<T: Equatable>
: Equatable
프로토콜
을 준수하는 모든 타입 T
댓글남기기