[Swift 문법] Swift 공식 문서 정리 - 22 - 제네릭(Generics)

윤여송·2023년 11월 8일
0

Swift

목록 보기
26/28
post-thumbnail

제너릭(Generics)

여러 타입에 대한 동작을 작성하고 해당 타입의 요구사항을 지정합니다.

제너릭 코드(Generic code)는 정의한 요구사항에 따라 모든 타입에서 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있습니다. 중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.

제너릭은 Swift의 강력한 특징 중 하나이고 Swift 표준 라이브러리 대부분은 제너릭 코드로 되어 있습니다. 예를 들어 Swift의 ArrayDictionary 타입은 둘다 제너릭 콜렉션 입니다. Int 값을 가진 배열, 또는 String 값을 가진 배열 또는 실제로 Swift에서 생성될 수 있는 다른 모든 타입에 대한 배열을 생성할 수 있습니다. 마찬가지로 모든 지정된 타입의 값을 저장하기 위한 딕셔너리를 생성할 수 있고 해당 타입에 대한 제한은 없습니다.

제너릭이 해결하는 문제 (The Problem That Generics Solve)

다음은 두 Int 값을 바꾸는 swapTwoInts(_:_:)라는 제너릭이 아닌 함수를 나타냅니다.

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

이 함수는 In-Out 파라미터에서 설명한 대로 ab의 값을 바꾸기 위해 in-out 파라미터를 사용하여 만듭니다.

swapTwoInts(_:_:) 함수는 b의 값을 a로 그리고 a의 값을 b로 바꿉니다. 2개의 Int 변수의 값을 바꾸기 위해 이 함수를 호출할 수 있습니다.

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3

swapTwoInts(_:_:) 함수는 유용하지만 Int 값만 사용이 가능 합니다. 2개의 String 값 또는 2개의 Double 값을 바꾸길 원하면 아래의 swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 함수와 같이 더 많은 함수를 작성해야 합니다.

func swapTwoString(_ a: inout String, _ b: inout String) {
	let temporaryA = a
    a = b
    b = temporaryA
}

func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
	let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoInts(_:_:), swapTwoStrings(_:_:), 그리고 swapTwoDoubles(_:_:) 함수의 본문이 동일하다는 것을 알 수 있습니다. 차이점은 받아들이는 값의 타입만 (Int, String, 그리고 Double) 다릅니다.

모든 타입의 2개의 값을 바꾸는 단일 함수로 작성하면 더 유용하고 더 유연합니다. 제너릭 코드는 이러한 함수를 작성할 수 있습니다.(이 함수의 제너릭 버전은 아래에 정의됩니다.)

Note
이 3개의 함수는 ab의 타입이 모두 같아야 합니다. ab가 같은 타입이 아니면 바꾸는 것은 불가능합니다. Swift는 타입 안전성 언어이고 String 타입의 변수와 Double 타입의 변수가 서로 값을 바꾸도록 허락하지 않습니다. 이러한 시도는 컴파일 에러가 발생합니다.

제너릭 함수(Generic Functions)

제너릭 함수(Generic functions)는 모든 타입과 함께 동작할 수 있습니다. 다음은 swapTwoValues(_:_:) 라는 위의 swapTwoInts(_:_:) 함수의 제너릭 버전입니다.

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

swapTwoValues(_:_:) 함수의 본문은 swapTwoInts(_:_:) 함수의 본문과 동일합니다. 그러나 swapTwoValues(_:_:) 의 첫번째 줄은 swapTwoInts(_:_:)와 약간 다릅니다. 다음은 첫번째 줄 차이의 비교입니다.

func swapTwoInts(_ a:inout Int, _ b: inout Int)
func swapTwoValue<T>(_ a: inout T, _ b: inout T)

함수의 제너릭 버전은 Int, String 또는 Double와 같은 실제 타입 이름 대신에 이 경우 T라는 임의의 타입 이름을 사용합니다. 이 임의의 타입 이름은 T가 무엇이어야 하는지 아무 말도 하지 않지만 T가 무엇을 나타내든 ab는 모두 같은 타입 T여야 한다고 말합니다. T의 실제 타입은 swapTwoValue(_:_:) 함수가 호출될 때마다 결정됩니다.

제너릭 함수와 제너릭이 아닌 함수 사이의 다른 차이점은 제너릭 함수의 이름(swapTwoValue(_:_:))에 바로 임의의 타입 이름 (T)이 꺽쇠 괄호 내 (<T>)에 위치한다는 것입니다. 이 괄호는 TswapTwoValue(_:_:) 함수 정의 내에서 임의의 타입 이름이라고 Swift에게 말합니다. T는 임의의 타입이므로 Swift는 T라는 실제 타입을 찾지 않습니다.

swapTwoValue(_:_:) 함수는 이제 swapTwoInts와 동일한 방식으로 호출될 수 있지만 두 값이 서로 동일한 타입이면 모든 타입의 두 값을 전달할 수 있다는 점이 다릅니다. swapTwoValue(_:_:)가 호출될 때마다 T로 사용한 타입은 함수에 전달된 값의 타입으로 부터 유추됩니다.

아래의 2개의 예제에서 T는 각각 IntString으로 추론됩니다.

var someInt = 3
var anotherInt = 107
swapTwoValue(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3

var someString = "hello"
var anotherString = "world"
swapTwoValue(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"

타입 파라미터 이름(Naming Type Parameters)

대부분의 경우 타입 파라미터는 타입 파라미터와 제너릭 타입 간의 관계나 함수 간의 관계를 나타내기 위해 Dictionary<Key, Value>에서 KeyValue 그리고 Array<Elementa>에서 Element와 같이 설명이 포함된 이름이 있습니다. 그러나 의미있는 관계가 없을 때는 위에서 swapTwoValue(_:_:) 함수에서 T와 같이 T, U, 그리고 V와 같은 단일 문자를 사용하여 이름을 지정하는 것이 일반적입니다.

제너릭 타입(Generic Types)

제너릭 함수 외에도 Swift는 고유한 제너릭 타입을 정의할 수 있습니다. 이것은 ArrayDictionary와 유사한 방식으로 모든 타입에서 동작할 수 있는 사용자 정의 클래스, 구조체, 그리고 열거형입니다.

이번 섹션은 Stack이라는 제너릭 콜렉션 타입을 어떻게 작성하는지 보여줍니다. 스택은 배열과 유사하지만 Swift의 Array 타입보다 더 제한된 작업 집합을 가진 순서가 지정된 집합입니다. 배열은 모든 위치에서 새 항목을 삽입하고 제거할 수 있습니다. 그러나 스택은 새로운 항목이 콜렉션의 끝에 추가하는 것만 허락합니다.(스택에 새로운 값을 푸쉬 한다고 알려져 있음). 마찬가지로 스택은 콜렉션의 끝부분의 항목만 제거할 수 있습니다.(스택에서 값을 팝 한다고 알려져 있음).

다음은 스택의 제너릭이 아닌 버전을 어떻게 작성하는지 나타내며 이 경우 Int 값의 스택에 대한 것을 보여줍니다.

struct IntStack {
	var items: [Int] = []
    mutating func push(_ item: Int) {
    	items.append(item)
    }
    mutating func pop() -> Int {
    	return items.removeLast()
    }
}

이 구조체는 스택에 값을 저장하기 위해 items라는 Array 프로퍼티를 사용합니다. Stack은 스택에 값을 푸쉬화고 팝하기 위해 pushpop인 2개의 메서드를 제공합니다. 구조체의 items 배열을 수정하거나 변경이 필요하므로 이 메서드는 mutating 으로 표시되어 있습니다.

그러나 위에 IntStack 타입은 Int 값만 사용할 수 있습니다. 모든 타입의 값으로 스택을 관리할 수 있는 제너릭 Stack 구조체를 정의하는 것이 더 유용합니다.

다음은 같은 코드의 제너릭 버전입니다.

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

Stack의 제너릭 버전은 기본적으로 제너릭이 아닌 버전과 동일하지만 Int의 실제 타입 대신에 Element라는 타입 파라미터를 사용합니다. 이 타입 파라미터는 구조체의 이름 바로뒤에 꺽쇠 괄호 내에 작성됩니다.

Element는 나중에 제공할 타입에 대한 임의의 이름을 정의합니다.
이 미래의 타입은 구조체의 정의 내 어디서나 Element로 참조될 수 있습니다. 이 경우에 Element는 아래의 3군데에서 임의로 사용됩니다.

  • Element 타입의 값으로 빈 배열로 초기화 되는 item라는 프로퍼티를 생성할 때
  • Element 타입이어야 하는 item 이라는 단일 파라미터를 가지는 push(_:) 메서드를 지정할 때
  • pop() 메서드에 의해 반환된 값이 Element 타입의 값으로 지정할 때

제너릭 타입이므로 StackArrayDictionary와 유사하게 Swift에서 모든 유효한 타입의 스택을 생성하기 위해 사용될 수 있습니다.

꺽쇠 괄호 내에 스택에 저장될 타입을 작성하여 새로운 Stack 인스턴스를 생성합니다. 예를 들어 문자열에 새로운 스택을 생성하려면 Stack<String>() 이라 작성합니다.

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")

제너릭 타입 확장(Extending a Generic Type)

제너릭 타입을 확장할 때 확장의 정의의 부분으로 타입 파라미터 리스트를 제공하지 않습니다. 대신에 기존 타입 정의에서 타입 파라미터 리스트는 확장의 본문 내에서 가능하고 기존 타입 파라미터 이름은 기존 정의에서 타입 파라미터를 참조하는데 사용됩니다.

다음의 예제는 스택에 팝 없이 스택의 가장 상단의 항목을 반환하는 topItem이라는 읽기전용 계산된 프로퍼티를 추가하기 위해 제너릭 Stack 타입을 확장합니다.

extension Stack {
	var topItem: Element? {
    	return items.isEmpty ? nil : items[items.count - 1]
    }
}

topItem 프로퍼티는 Element 타입의 옵셔널 값을 반환합니다. 스택이 비어있다면 topItemnil을 반환하고 스택이 비어 있지 않다면 topItemitems 배열에서 마지막 항목을 반환합니다.

이 확장은 타입 파라미터 리스트를 정의하지 않습니다. 대신에 Element 라는 Stack 타입의 존재하는 타입 파라미터 이름은 topItem 계산된 프로퍼티의 옵셔널 타입임을 나타내기 위해 확장 내에서 사용됩니다.

topItem 계산된 프로퍼티는 최상단 항목의 삭제없이 접근하고 조회하기 위해 모든 Stack 인스턴스에서 사용될 수 있습니다.

if let topItem = stackOfStrings.topItem {
	print("The top item on the stack is \(topItem).")
}

타입 제약(Type Constraints)

profile
y_some__velog

0개의 댓글