[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타입의 매개변수itempop()메서드에서 반환되는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
댓글남기기