study record

[Swift] 제네릭 본문

Swift/스위프트 프로그래밍

[Swift] 제네릭

asong 2022. 1. 14. 10:05

*이 글은 책 “스위프트 프로그래밍”을 읽고 작성된 글입니다.

 

 

제네릭은 스위프트의 강력한 기능 중 하나이다. 제네릭을 이용해 코드를 구현하면 어떤 타입에도 유연하게 대응할 수 있다. 또한 제네릭으로 구현한 기능과 타입은 재사용하기도 쉽고, 코드의 중복을 줄일 수 이씨에 깔끔하고 추상적인 표현이 가능하다.

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

제네릭 함수

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

제네릭함수는 실제 타입 이름을 써주는 대신에 플레이스 홀더(T)를 사용한다. 플레이스홀더는 타입의 종류를 알려주지 않지만 말 그대로 어떤 타입이라는 것은 알려준다. 두 매개변수의 타입이 모두 T이므로 같은 타입이라는 것을 알 수 있다. T의 실제 타입은 함수가 호출되는 순간 결정된다.

타입 매개변수를 지정해주면 이를 함수의 매개변수의 타입으로 사용할 수 있다. 또는 함수의 반환타입으로 사용할 수도 있으며, 함수 내부 변수의 타입 지정을 위해 사용할 수도 있다. 각각의 경우 타입 매개변수는 함수를 호출할 때마다 실제 타입으로 치환된다. 여러 개의 타입 매개변수를 가지고 싶다면 홀화살괄호 기호 안 쪽에 쉼표로 분리한 여러 개의 타입 매개변수를 지정해줄 수 있다. <T, U, V>

타입 매개변수 대부분은 의미있는 이름을 가지게 되는 경우가 많다. 예로 Dictionary<Key, Value>, Array<Element>처럼 표현할 수 있다.

 

제네릭 타입

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

struct Stack<Element> {
	var items = [Element]()
	muating func push(_ item: Element) {
		items.append(item)
	}
	mutating func pop() -> Element {
		return items.removeLast()
	}
}

 

제네릭 타입 확장

익스텐션을 통해 제네릭을 사용하는 타입에 기능을 추가하고자 한다면 익스텐션 정의에 타입 매개변수를 명시하지 않아야 한다. 대신 원래의 제네릭 정의에 명시한 타입 매개변수를 익스텐션에서 사용할 수 있다.

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

이 익스텐션은 Stack 구조체를 확장한 것이다. Stack은 제네릭 타입이지만 익스텐션의 정의에는 따로 타입 매개변수를 명시하지 않았다. 대신 기존의 제네릭 타입에 정의되어 있는 Element 타입을 사용할 수 있다.

 

타입 제약

제네릭 기능의 타입 매개변수는 실제 사용 시 타입의 제약 없이 사용할 수 있었다. 그러나 제네릭 함수가 처리해야 할 기능이 특정 타입에 한정되어야만 처리할 수 있거나, 제네릭 타입을 특정 프로토콜을 따르는 타입만 사용할 수 있도록 제약을 주어야 하는 상황이 생길 수 있다.

타입 제약은 타입 매개변수가 가져야 할 제약사항을 지정할 수 있는 방법이다. 타입 제약은 클래스 타입 또는 프로토콜로만 줄 수 있다. 즉 열거형, 구조체 등의 타입은 사용할 수 없다.

예시로 Dictionary의 키는 Hashable 프로토콜을 준수하는 타입만 사용할 수 있다.

func swapTwoValues<T: BinaryInteger>(_ a: inoutT, _ b: inout T) {

}

func swapTwoValues<T: BinaryInteger>(_ a: inoutT, _ b: inout T) where T: FloatingPoint  {

}

타입 매개변수 뒤에 콜론을 붙이고 제약조건으로 주어질 타입을 명시하면 된다. 여러 제약을 추가하고 싶다면 콤마로 구분하는 것이 아니라 where 절을 사용할 수 있다.

 

프로토콜의 연관 타입

프로토콜을 정의할 때 연관 타입을 함께 정의하면 유용할 때가 있다. 연관 타입이란 프로토콜에서 사용할 수 있는 플레이스홀더 이름이다. 즉, 제네릭에서는 어떤 타입이 들어올지 모를 때, 타입 매개변수를 통해 어떤 타입이 여기에 쓰일 것이다라고 표현해주었다면 연관 타입은 타입매개변수의 그 역할을 프로토콜에서 사용할 수 있도록 만들어진 기능이다.

protocol Container {
	associatedtype ItemType
	var count: Int { get }
	mutating func append(_ item: ItemType)
	subscript(i: Int) -> ItemType { get }
}

Container 프로토콜은 존재하지 않는 타입인 ItemType을 연관 타입으로 정의하여 프로토콜 정의에서 타입 이름으로 활용한다. 이는 제네릭의 타입 매개변수와 유사하다. 프로토콜 정의 내부에서 사용할 타입이 어떤 것이어도 상관없지만 하나의 타입임은 분명하다라는 의미이다.

class MyContainer: Container {
	var items: Array<Int> = Array<Int>()

	var count: Int {
		return tiems.count
	}

	func append(_ item: Int) {
		items.append(item)
	}

	subscript(i: Int) -> Int {
		return items[i]
	}
}

MyContainer 클래스는 프로토콜을 준수하기 위해서 필요한 것을 모두 갖추었다. 실제 프로토콜 정의를 준수하기 위해 구현할 때는 ItemType을 하나의 타입으로 일관성 있게 구현하면 된다.

 

제네릭 서브스크립트

제네릭 함수를 구현할 수 있었던 것처럼 서브스크립트도 제네릭을 활용하여 타입에 큰 제한 없이 유연하게 구현할 수 있다. 타입 제약을 사용하여 제네릭을 활용하는 타입에 제약을 줄 수도 있다.

'Swift > 스위프트 프로그래밍' 카테고리의 다른 글

[Swift] 타입 중첩  (0) 2022.01.19
[Swift] 프로토콜 지향 프로그래밍  (0) 2022.01.18
[Swift] 익스텐션  (0) 2022.01.12
[Swift] 프로토콜  (0) 2022.01.11
[Swift] 타입 캐스팅  (0) 2022.01.06