xho95 (소중한꿈)'s Swift Life

Apple 에서 공개한 The Swift Programming Language (Swift 5.7) 책의 Generics 부분1을 번역하고, 설명이 필요한 부분은 주석을 달아서 정리한 글입니다. 전체 번역은 Swift 5.7: Swift Programming Language (스위프트 프로그래밍 언어) 에서 확인할 수 있습니다.

Generics (일반화)

일반화 코드 (generic code) 는, 직접 정의한 필수 조건[^requirements] 을 전제로 하여, 어떤 타입과도 작업할 수 있는 유연하고, 재사용 가능한 함수와 타입을 작성할 수 있게 합니다. 중복은 피하면서 의도는 명확하고, 추상적으로 표현하는 코드를 작성할 수 있습니다.

일반화는 스위프트의 가장 강력한 특징 중 하나이며, 스위프트 표준 라이브러리의 많은 곳이 일반화 코드로 제작되었습니다. 사실, 알지 못했겠지만, 언어 설명서 (Language Guide)2 전반에 걸쳐 일반화를 계속 사용했습니다. 예를 들어, 스위프트의 ArrayDictionary 타입 모두 일반화 집합체 (genenric colletion) 입니다. Int 값을 쥔 배열이나, String 값을 쥔 배열, 또는 진짜로 스위프트가 생성할 수 있는 다른 어떤 타입의 배열이든 생성할 수 있습니다. 이와 비슷하게, 어떤 특정 타입의 값을 저장한 딕셔너리도 생성할 수 있는데, 그 타입이 뭐가 될 수 있는 지엔 한계가 없습니다.

The Problem That Generics Solve (일반화로 푸는 문제)

Int 값을 맞바꾸는, swapTwoInts(_:_:) 라는 표준, 일반화 아닌 함수는 이렇습니다:

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

이 함수는, In-Out Parameters (입-출력 매개 변수) 에서 설명한, 입-출력 매개 변수를 사용하여 ab 의 값을 맞바꿉니다.

swapTwoInts(_:_:) 함수는 b 의 원본 값을 a 로, a 의 원본 값은 b 로, 맞바꿉니다. 이 함수를 호출하면 두 Int 변수의 값을 맞바꿀 수 있습니다:

var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// "someInt is now 107, and anotherInt is now 3" 를 인쇄함

swapTwoInts(_:_:) 함수는 유용하지만, Int 값에만 쓸 수 있습니다. 두 String 값이나, 두 Double 값을 맞바꾸고 싶으면, 밑에서 보는 swapTwoStrings(_:_:)swapTwoDoubles(_:_:) 함수를, 더 작성해야 합니다:

func swapTwoStrings(_ 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) 입니다.

어떤 (any) 타입의 두 값도 맞바꾸는 단일 함수를 작성하는 게, 더 유용하며, 또 더 유연합니다. 일반화 코드는 그러한 함수를 작성할 수 있게 합니다. (이 함수들의 일반화 버전은 밑에서 정의합니다.)

모든 세 함수에서, ab 의 타입은 반드시 똑같아야 합니다. ab 의 타입이 똑같지 않으면, 이들의 값을 맞바꾸는게 불가능합니다. 스위프트는 타입-안전 언어이며, (예를 들어) String 타입의 변수와 Double 타입의 변수가 서로 값을 맞바꾸는 걸 허용하지 않습니다. 그러한 시도는 컴파일-시간 에러가 됩니다.

Generic Functions (일반화 함수)

일반화 함수는 어떤 타입과도 일할 수 있습니다. 위에 있는 swapTwoInts(_:_:) 함수의 일반화 버전인, swapTwoValues(_: _:) 는 이렇습니다:

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 swapTwoValues<T>(_ a: inout T, _ b: inout T)

일반화 버전의 함수는 (Int 나, String, 및 Double 같은) 실제 (actual) 타입 이름 대신 자리 표시용 (placeholder) 타입 이름을 (이 경우엔, T 를) 사용합니다. 자리 표시용 타입 이름은 T 가 무엇이어야 하는지에 대해선 어떤 것도 말하지 않지만, T 가 뭘 나타내든, ab 둘 다 반드시 동일한 타입 T 여야 한다 (does) 는 걸 말합니다. T 자리에 사용할 실제 타입은 swapTwoValues​​(_:_:) 함수를 호출할 때마다 결정합니다.

일반화 함수와 일반화 아닌 함수 사이의 다른 차이점은 일반화 함수 이름 (swapTwoValues​​(_:_:)) 뒤의 자리 표시용 타입 이름 (T) 은 꺾쇠 괄호 (<T>) 안에 둔다는 겁니다. 괄호는 스위프트에게 swapTwoValues​​(_:_:) 함수 정의 안의 T 가 자리 표시용 타입 이름이라는 걸 말합니다. T 가 자리 표시자이기 때문에, 스위프트는 T 라는 실제 타입을 찾진 않습니다.

swapTwoValues​​(_:_:) 함수는 이제, 두 값이 서로 동일한 타입인 한, 어떤 (any) 타입의 두 값도 전달할 수 있음을 제외하면, swapTwoInts 와 똑같은 식으로 호출할 수 있습니다. swapTwoValues​​(_:_:) 를 호출할 때마다, 함수에 전달한 값의 타입으로 T 에 사용할 타입을 추론합니다.

아래의 두 예제에선, T 가 각각 IntString 이라고 추론합니다:

var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt 는 이제 107 이고, anotherInt 는 이제 3 임

var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString 은 이제 "world" 이고, anotherString 은 이제 "hello" 임

위에서 정의한 swapTwoValues(_:_:) 함수는, 스위프트 표준 라이브러리의 일부인, swap 이라는 일반화 함수에서 영감을 받은 것으로, 앱에서 쓸 수 있게 자동으로 만들어집니다. 자신의 코드에 swapTwoValues(_:_:) 함수의 동작이 필요하다면, 직접 구현하지 말고 스위프트에 있는 swap(_:_:) 함수를 쓰면 됩니다.

Type Parameters (타입 매개 변수)

위의 swapTwoValues(_:_:) 예제에서, 자리 표시용 타입 T타입 매개 변수 (type parameter) 의 한 예입니다. 타입 매개 변수는 자리 표시용 타입을 지정하여 이름을 붙이며, 함수 이름 바로 뒤에, (<T> 같이) 일치하는 꺽쇠 괄호 쌍 사이에 작성합니다.

타입 매개 변수를 한 번 지정하고 나면, 이를 사용하여 (swapTwoValues(_:_:) 함수의 ab 매개 변수 같은) 함수 매개 변수의 타입이나, 함수 반환 타입, 또는 함수 본문 안의 타입 보조 설명3 을 정의할 수 있습니다. 각각의 경우, 함수를 호출할 때마다 타입 매개 변수를 실제 (actual) 타입으로 교체합니다. (위의 swapTwoValues(_:_:) 예제에선, 첫 번째로 함수를 호출할 땐 TInt 로 교체하고, 두 번째로 호출할 땐 String 으로 교체합니다.)

하나 이상의 타입 매개 변수를 제공하려면 꺾쇠 괄호 안에 여러 개의 타입 매개 변수 이름을, 쉼표로 구분하여, 작성하면 됩니다.

Naming Type Parameters (타입 매개 변수 이름짓기)

대부분의 경우, 타입 매개 변수는, Dictionary<Key, Value> 안의 KeyValueArray<Element> 안의 Element 같이, 타입 매개 변수와 이를 사용한 일반화 타입 또는 함수 간의 관계를, 설명하는 이름을 가집니다. 하지만, 이들 간의 관계가 의미가 없을 땐, 전통적으로, 위의 swapTwoValues(_:_:) 함수의 T 같이, T, U, 및 V 같은 단일 문자로 이름을 짓습니다.

타입 매개 변수엔 항상 (TMyTypeParamter 같은) 낙타 모양 대문자 (upper camel case)4 이름을 줘서, 값이 아닌, 타입 (type) 의 자리 표시자라는 걸 지시합니다.

Generic Types (일반화 타입)

일반화 함수에 더하여, 스위프트는 자신만의 일반화 타입 (generic types) 도 정의할 수 있게 합니다. 이들은, ArrayDictionary 와 비슷하게, 어떤 (any) 타입과도 작업할 수 있는 사용자 정의 클래스, 구조체, 및 열거체입니다.

이 절에선 Stack 이라는 일반화 집합체 타입의 작성 방법을 보입니다. 스택 (stack) 은, 배열과 비슷하게, 순서 있는 (ordered) 값 집합5 이지만, 스위프트의 Array 타입보다 더 제약된 (restricted) 연산 집합을 가집니다.6 배열은 배열의 어떤 위치에서든 새 항목을 집어 넣고 제거하는 걸 허용합니다. 하지만, 스택은 집합체 끝에서만 새 항목을 덧붙이는 (스택에 새 값 밀어 넣기 (pushing) 라는) 것만을 허용합니다. 이와 비슷하게, 스택은 집합체 끝에서만 항목을 제거하는 (스택에서 값 빼내기 (popping) 라는) 것만을 허용합니다.

스택이라는 개념은 UINavigationController 클래스의 항법 계층 구조의 뷰 컨트롤러들을 모델링할 때 사용합니다. UINavigationController 클래스의 pushViewController(_:animated:) 메소드를 호출하여 항법 스택에 뷰 컨트롤러를 추가 (또는 밀어 넣으며), 자신의 popViewControllerAnimated(_:) 메소드로 항법 스택에 있는 뷰 컨트롤러를 제거 (또는 빼냅니다). 스택은 엄격한 “후입 선출법 (last in, first out)”7 접근법으로 집합체를 관리할 필요가 있을 때마다 유용한 집합체 모델입니다.

아래 삽화는 스택의 밀어넣기와 빼내기 동작을 보여줍니다:

Push and Pop of Stack

  1. 스택에 현재 세 개의 값이 있습니다.
  2. 네 번째 값을 스택 맨 위에 밀어 넣습니다.
  3. 스택은 이제 네 값을 쥐고 있으며, 가장 최근 게 맨 위에 있습니다.
  4. 스택 맨 위의 항목을 빼냅니다.
  5. 값을 빼낸 후의, 스택은 다시 한번 세 개의 값을 쥐고 있습니다.

일반화 아닌 버전의 스택 중, 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 이라는, 두 메소드를 제공하여, 스택에 값을 밀어 넣고 빼냅니다. 이 메소드들은 mutating 이라고 표시하는데, 구조체의 items 배열을 수정 (또는 변경 (mutate) 할) 필요가 있기 때문입니다.

하지만, 위에 보인 IntStack 타입은 Int 값 하고만 사용할 수 있습니다. 일반화 (generic) Stack 클래스를 정의하여, 어떤 (any) 타입의 값이든 스택으로 관리할 수 있는게, 훨씬 더 유용할 것입니다.

동일한 코드에 대한 일반화 버전은 이렇습니다:

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 라고 참조할 수 있습니다. 이 경우엔, 세 곳에서 Element 라는 자리 표시자를 사용합니다:

일반화 타입이기 때문에, Stack 을 사용하면, ArrayDictionary 와 비슷하게, 스위프트의 어떤 (any) 유효 타입에 대한 스택도 생성할 수 있습니다.

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

var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 스택은 이제 4 개의 문자열을 담음

이 네 값을 스택에 밀어 넣은 후의 stackOfStrings 은 이렇게 보입니다:

Pushing of Stack

스택에서 값을 빼내면, "cuatro" 라는, 맨 위의 값을 제거하고 반환합니다:

let fromTheTop = stackOfStrings.pop()
// fromTheTop 은 "cuatro" 와 같고, 스택은 이제 3 개의 문자열을 담음

자신의 맨 위 값을 빼낸 후의 스택은 이렇게 보입니다:

Popping of Stack

Extending a Generic Type (일반화 타입 확장하기)

일반화 타입을 확장할 땐, 익스텐션의 정의 부분에 타입 매개 변수 목록을 제공하지 않습니다.8 그 대신, 익스텐션 본문 안에서 원본 (original) 타입 정의에 있는 타입 매개 변수 목록을 사용하는게 가능하며, 원본 타입 매개 변수 이름을 사용하여 원본 정의에 있는 타입 매개 변수를 참조합니다.

다음 예제는 일반화 Stack 타입을 확장하여 topItem 이라는 읽기-전용 계산 속성을 추가하는데, 이는 스택에서 맨 위의 항목을 빼내지 않고 이를 반환합니다:

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

topItem 속성은 Element 타입에 대한 옵셔널 값을 반환합니다. 스택이 비었으면, topItemnil 을 반환하며; 스택이 비지 않았으면, topItemitems 배열의 최종 항목을 반환합니다.

이 익스텐션은 타입 매개 변수 목록을 정의하지 않음을 기억하기 바랍니다. 그 대신, 익스텐션 안에서 Stack 타입의 기존 타입 매개 변수 이름인, Element 를, 사용하여 topItem 계산 속성의 옵셔널 타입을 지시합니다.

이제 어떤 Stack 인스턴스로도 topItem 계산 속성을 사용하면 자신의 맨 위 항목을 제거하지 않고 이에 접근하고 조회할 수 있습니다.

if let topItem = stackOfStrings.topItem {
  print("The top item on the stack is \(topItem).")
}
// "The top item on the stack is tres." 를 인쇄함

밑에 있는 Extensions with a Generic Where Clause (일반화 where 절이 있는 익스텐션) 에서 논의한 것처럼, 일반화 타입의 익스텐션에 필수 조건도 포함하여 확장한 타입의 인스턴스가 반드시 만족해야 새 기능을 얻도록 할 수 있습니다.

Type Constraints (타입 구속 조건)

swapTwoValues​​(_:_:) 함수와 Stack 타입은 어떤 타입과도 작업할 수 있습니다. 하지만, 타입에 특정 타입 구속 조건 (type constraints) 을 강제하여 일반화 함수 및 일반화 타입과 사용할 수 있게 하는게 유용할 때가 있습니다. 타입 구속 조건은 타입 매개 변수가 반드시 정해진 클래스를 상속하거나, 한 특별한 프로토콜 또는 프로토콜 합성9 을 준수하기를 지정합니다.

예를 들어, 스위프트의 Dictionary 타입은 딕셔너리의 키로 사용할 수 있는 타입에 제한을 둡니다. Dictionaries (딕셔너리) 에서 설명한 것처럼, 딕셔너리 키의 타입은 반드시 해시 가능 (hashable)10 해야 합니다. 즉, 반드시 그 자신을 유일하게 나타낼 수 있는 방법을 제공해야 합니다. Dictionary 는 자신의 키가 해시 가능할 필요가 있는데 그래야 한 특별한 키에 이미 값이 담겼는지 검사할 수 있습니다. 이 필수 조건 없이는, Dictionary 가 한 특별한 키에 값을 집어 넣어야 할지 교체해야 할지 말할 수 없으며, 주어진 키의 값이 딕셔너리에 이미 있는 데도 찾을 수 없을 것입니다.

Dictionary 키 타입의 타입 구속 조건이 이 필수 조건을 강제하는데, 이는 키 타입이, 스위프트 표준 라이브러리가 정의한 특수한 프로토콜인, Hashable 프로토콜을 반드시 준수할 것을 지정합니다. (String, Int, Double, 및 Bool 같은) 스위프트의 모든 기초 타입은 기본적으로 해시 가능합니다. 자신의 타입이 Hashable 프로토콜을 준수하게 하는 방법에 대한 정보는, Conforming to the Hashable Protocol11 항목을 보도록 합니다.

자신의 일반화 타입을 생성할 때 자신만의 타입 구속 조건을 정의할 수 있으며, 이러한 구속 조건이 일반화 프로그래밍12 의 대부분의 강력함을 제공합니다. Hashable 같은 추상적인 개념 (abstract concepts) 은 타입을, 자신의 고정 타입 (concrete type) 보단, 자신의 개념적인 성질 (conceptual characteristics) 로 성격짓습니다.13

Type Constraint Syntax (타입 구속 조건 구문)

타입 구속 조건은, 타입 매개 변수 목록 부분에서, 타입 매개 변수 이름 뒤에, 콜론으로 구분한, 단일 클래스나 프로토콜 구속 조건을 둬서 작성합니다. 일반화 함수의 타입 구속 조건에 대한 기초 구문을 아래에 보입니다 (일반화 타입도 구문은 똑같습니다):

func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
  // 함수 본문은 여기에 둠
}

위에 있는 가상의 함수에는 타입 매개 변수가 두 개 있습니다. 첫 번째 타입 매개 변수인, T 에는, TSomeClass 의 하위 클래스일 걸 요구하는 타입 구속 조건이 있습니다. 두 번째 타입 매개 변수인, U 에는, USomeProtocol 프로토콜을 준수할 걸 요구하는 타입 구속 조건이 있습니다.

Type Constraints in Action (타입 구속 조건의 실제 사례)

findIndex(ofString:in:) 이라고, 찾을 String 값과 찾을 String 값이 담긴 배열이 주어지는, 일반화 아닌 함수는 이렇습니다. findIndex(ofString:in:) 함수는 옵셔널 Int 값을 반환하는데, 배열에서 문자열을 찾았으면 첫 번째로 일치한 거의 색인일거고, 문자열을 못찾았으면 nil 일 겁니다:

func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
  for (index, value) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

findIndex(ofString:in:) 함수를 사용하면 문자열 배열에서 문자열 값을 찾을 수 있습니다:

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" 를 인쇄함

하지만, 배열에서 값의 색인을 찾는 원리는 문자열에만 유용한 게 아닙니다. 문자열이라고 언급한 어떤 곳이든 어떠한 타입 T 의 값이라고 대체함으로써 동일한 기능을 일반화 함수로 작성할 수 있습니다.

findIndex(ofString:in:) 의 일반화 버전인, findIndex(of:in:) 라고, 예상할 법한 걸 작성하면 이렇습니다. 함수가, 배열에 있는 옵셔널 값이 아닌, 옵셔널 색인 번호를 반환하기 때문에, 이 함수의 반환 타입이 여전히 Int? 라는 걸 기억하기 바랍니다. 그렇지만, 경고 하건데-이 함수는, 예제 뒤에 설명할 이유로 인해, 컴파일되지 않습니다:

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
  for (index, value) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

위에 적은 대로 이 함수는 컴파일되지 않습니다. 문제는, “if value == valueToFind” 라는, 같음 비교 검사에 있습니다. 스위프트의 모든 타입을 같음 비교 연산자 (==) 로 비교할 수 있는 건 아닙니다. 예를 들어, 복잡한 데이터 모델을 나타내고자 자신만의 클래스나 구조체를 생성한다면, 그 클래스나 구조체에서의 “같음 (equal to)” 의 의미는 스위프트가 추측할 수 있는 어떤 게 아닙니다. 이 때문에, 가능한 모든 (every) 타입 T 에 대해 이 코드가 일할거라는 보증이 불가능하여, 코드를 컴파일하려 할 때 적절한 에러를 보고합니다.

하지만, 모든 걸 잃진 않습니다. 스위프트 표준 라이브러리는 Equatable 이라는 프로토콜을 정의하는데, 이는 어떤 준수 타입이든 같음 비교 연산자 (==) 와 같지 않음 비교 연산자 (!=) 를 구현하여 그 타입의 어떤 두 값이든 비교하도록 요구합니다. 스위프트의 모든 표준 타입은 자동으로 Equatable 프로토콜을 지원합니다.

Equaltable 인 어떤 타입도 findIndex(of:in:) 함수를 안전하게 사용할 수 있는데, 이는 같음 비교 연산자의 지원을 보증하기 때문입니다. 이 사실을 표현하려면, 함수를 정의할 때 타입 매개 변수 정의 부분에서 Equatable 이라는 타입 구속 조건을 작성합니다:

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
  for (index, value) in array.enumerated() {
    if value == valueToFind {
      return index
    }
  }
  return nil
}

findIndex(of:in:) 의 단일 타입 매개 변수는 T: Equatable 이라고 작성하는데, 이는 “Equatable 프로토콜을 준수하는 어떤 타입 T” 이라는 의미입니다.

findIndex(of:in:) 함수는 이제 컴파일 성공하며, Double 이나 String 같은, Equatable 인 어떤 타입과도 사용할 수 있습니다:

let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex 는 값이 없는 옵셔널 Int 인데, 배열에 9.3 이 없기 때문임
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex 는 2 라는 값을 담은 옵셔널 Int 임

Associated Types (결합 타입)

프로토콜을 정의할 때, 프로토콜 정의 부분에서 하나 이상의 결합 타입을 선언하는 게 유용할 때가 있습니다. 결합 타입 (associated type) 은 프로토콜에서 사용할 타입에 자리 표시용 이름14 을 줍니다. 그 결합 타입에 사용할 실제 타입은 프로토콜 채택 전까진 지정하지 않습니다. 결합 타입은 associatedtype 키워드로 지정합니다.15

Associated Types in Action (결합 타입의 실제 사례)

Item 이라는 결합 타입을 선언한, Container 라는 프로토콜 예제는 이렇습니다:

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

Container 프로토콜은 어떤 컨테이너라도 반드시 제공해야 할 필수 보유 능력 (required capabilities) 세 개를 정의합니다:

이 프로토콜은 컨테이너에 항목을 저장하는 방법이나 무슨 타입(의 저장)을 허용하는지는 지정하지 않습니다. 프로토콜은 어떤 타입이든 Container 라고 하기 위해선 반드시 제공해야 할 세 개의 작은 기능만을 지정합니다. 준수 타입은, 이 세 필수 조건을 만족하는 한, 추가 기능을 제공할 수도 있습니다.

Container 프로토콜을 준수한 어떤 타입이든 자신이 저장할 값의 타입을 반드시 지정할 수 있어야 합니다. 특히, 올바른 타입의 항목만 컨테이너에 추가함을 반드시 보장해야 하며, 자신의 첨자가 반환할 항목의 타입도 반드시 명확해야 합니다.

이러한 필수 조건을 정의하려면, 특정 컨테이너가 보유할 원소 타입이 뭔지 알지 않고도, Container 프로토콜이 그 타입을 참조할 방법이 필요합니다. Containter 프로토콜은 append(_:) 메소드에 전달할 어떤 값도 반드시 컨테이너 원소 타입과 동일한 타입일 것과, 컨테이너의 첨자가 반환할 값이 반드시 컨테이너 원소 타입과 동일한 타입일 것을, 지정할 필요가 있습니다.

이를 달성하기 위해, Container 프로토콜은 Item 이라는 결합 타입을, associatedtype Item 이라고 작성하여, 선언합니다. Item 이 뭔지는 프로토콜이 정의하지 않습니다-그 정보는 어떤 준수 타입이든 알아서 제공하게 남겨둡니다. 그럼에도 불구하고, Item 이란 별명은 Container 항목의 타입을 참조하고, append(_:) 메소드와 첨자에 사용할 타입을 정의하며, 예상한 어떤 Container 동작이든 강제로 하도록 보장할, 방법을 제공합니다.

위에 있는 Generic Types (일반화 타입) 의 일반화 아닌 IntStack 타입 버전이, Container 프로토콜을 준수하도록 개조하면, 이렇습니다:

struct IntStack: Container {
  // 원본 IntStack 구현
  var items = [Int]()
  mutating func push(_ item: Int) {
    items.append(item)
  }
  mutating func pop() -> Int {
    return items.removeLast()
  }
  // Container 프로토콜의 준수
  typealias Item = Int
  mutating func append(_ item: Int) {
    self.push(item)
  }
  var count: Int {
    return items.count
  }
  subscript(i: Int) -> Int {
    return items[i]
  }
}

IntStack 타입은 Container 프로토콜의 모든 세 필수 조건을 구현하며, 각각의 경우마다 IntStack 타입의 기존 기능 일부를 포장하여 이러한 필수 조건을 만족합니다.

게다가, IntStack 은 이 Container 구현이, 사용하기 적적한 Item 타입은 Int 라는 걸 지정합니다. typealias Item = Int 라는 정의는 이 Container 프로토콜 구현에서 Item 이라는 추상 타입을 Int 라는 고정 타입으로 바꿉니다.

고맙게도 스위프트의 타입 추론 덕분에, 실제론 IntStack 정의 부분에서 ItemInt 라고 고정하여 선언할 필요가 없습니다.16 IntStackContainer 프로토콜의 모든 필수 조건을 준수하기 때문에, append(_:) 메소드의 item 매개 변수 타입과 첨자의 반환 타입을 단순히 살펴 봄으로써, 스위프트가 사용하기 적절한 Item 을 추론할 수 있습니다. 진짜로, 위 코드에서 typealias Item = Int 줄을 지워도, Item 에 무슨 타입을 쓸지 명확하기 때문에, 모든 것이 여전히 작동합니다.

일반화 Stack 타입이 Container 프로토콜을 준수하게 할 수도 있습니다:

struct Stack<Element>: Container {
  // 원본 Stack<Element> 구현
  var items = [Element]()
  mutating func push(_ item: Element) {
    items.append(item)
  }
  mutating func pop() -> Element {
    return items.removeLast()
  }
  // Container 프로토콜의 준수
  mutating func append(_ item: Element) {
    self.push(item)
  }
  var count: Int {
    return items.count
  }
  subscript(i: Int) -> Element {
    return items[i]
  }
}

이번엔, Element 타입 매개 변수를 append(_:) 메소드의 item 매개 변수 타입과 첨자의 반환 타입으로 사용합니다. 그럼으로써 이 특별한 컨테이너의 Item 으로 사용하기 적절한 타입이 Element 라는 걸 스위프트가 추론할 수 있습니다.

Extending an Existing Type to Specify an Associated Type (기존 타입을 확장하여 결합 타입 지정하기)

Adding Protocol Conformance with an Extension (익스텐션으로 프로토콜 준수성 추가하기) 에서 설명한 것처럼, 기존 타입을 확장하여 프로토콜에 준수성 (conformance) 을 추가할 수 있습니다. 이는 결합 타입이 있는 프로토콜도 포함합니다.

스위프트의 Array 타입은 append(_:) 메소드, count 속성, 및 Int 색인으로 자신의 원소를 가져오는 첨자를 이미 제공합니다. 이러한 세 보유 능력은 Container 프로토콜의 필수 조건과 일치합니다. 이는 ArrayContainer 프로토콜을 준수하도록 확장하려면 단순히 Array 가 프로토콜을 채택한다고 선언하면 된다는 의미입니다. 이는, Declaring Protocol Adoption with an Extension (익스텐션으로 프로토콜 채택 선언하기) 에서 설명한 것처럼, 빈 (empty) 익스텐션을 가지고 합니다:

extension Array: Container {}

Array 의 기존 append(_:) 메소드와 첨자는, 그냥 위의 일반화 Stack 타입 처럼, 스위프트가 Item 으로 사용할 적절한 타입을 추론할 수 있게 합니다. 이 익스텐션의 정의 후엔, 어떤 ArrayContainer 로 사용할 수 있습니다.

Adding Constraints to an Associated Type (결합 타입에 구속 조건 추가하기)

프로토콜의 결합 타입에 타입 구속 조건을 추가하여 준수 타입이 그 구속 조건을 만족하길 요구할 수 있습니다. 예를 들어, 다음 코드는 컨테이너 항목의 같음 비교가 가능하길 요구한 Container 버전을 정의합니다:17

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

이 버전의 Container 를 준수하려면, 컨테이너의 Item 타입이 Equatable 프로토콜을 준수해야 합니다.

Using a Protocol in Its Associated Type’s Constraints (자신의 결합 타입 구속 조건에서 프로토콜 사용하기)

프로토콜은 자신의 필수 조건 부분에 나타날 수 있습니다. 예를 들어, Container 프로토콜을 개량하여, suffix(_:) 메소드라는 필수 조건을 추가한, 프로토콜은 이렇습니다. suffix(_:) 메소드는 컨테이너 끝에서부터 주어진 개수의 원소를, Suffix 타입의 인스턴스에 저장하여, 반환합니다.

protocol SuffixableContainer: Container {
  associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
  func suffix(_ size: Int) -> Suffix
}

이 프로토콜의, Suffix 는, 위의 Containter 예제의 Item 타입 같은, 결합 타입입니다. Suffix 에는 구속 조건이 두 개 있는데: (현재 정의하고 있는 프로토콜인) SuffixableContainer 프로토콜을 반드시 준수해야 한다는 것과, 자신의 Item 타입과 컨테이너의 Item 타입이 반드시 똑같아야 한다는 것입니다. Item 에 대한 구속 조건은, 밑에 있는 Associated Types with a Generic Where Clause (일반화 where 절이 있는 결합 타입) 에서 논의할, 일반화 where 절입니다.

위의 Generic Types (일반화 타입) 에 있는 Stack 타입에 SuffixableContainer 프로토콜 준수성을 추가하는 익스텐션은 이렇습니다:

extension Stack: SuffixableContainer {
  func suffix(_ size: Int) -> Stack {
    var result = Stack()
    for index in (count-size)..<count {
      result.append(self[index])
    }
    return result
  }
  // Suffix 는 Stack 이라고 추론함
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 는 20 과 30 을 담고 있음

위 예제에서, StackSuffix 결합 타입도 Stack 이라, Stack 의 접미사 (suffix) 연산도 또 다른 Stack 를 반환합니다. 대안으로, SuffixableContainer 를 준수한 타입이 자기 자신과는 다른 Suffix 타입을 가질 수 있는데-이는 접미사 연산이 다른 타입을 반환할 수 있다는 의미입니다. 예를 들어, 일반화 아닌 IntStack 타입이, 자신의 접미사 타입으로 IntStack 대신 Stack<Int> 를 사용하도록, SuffixableContainer 준수성을 추가한, 익스텐션은 이렇습니다:

extension IntStack: SuffixableContainer {
  func suffix(_ size: Int) -> Stack<Int> {
    var result = Stack<Int>()
    for index in (count-size)..<count {
      result.append(self[index])
    }
    return result
  }
  // Suffix 는 Stack<Int> 라고 추론함
}

Generic Where Clauses (일반화 ‘where’ 절)

타입 구속 조건은, Type Constraints (타입 구속 조건) 에서 설명한 것처럼, 일반화 함수나, 첨자, 또는 타입과 결합한 타입 매개 변수에 필수 조건을 정의할 수 있게 합니다.

결합 타입에 필수 조건을 정의하는 게 유용할 수도 있습니다. 이렇게 하려면 일반화 where 절 (generic where clause) 을 정의하면 됩니다. 일반화 where 절은 결합 타입이 특정 프로토콜을 반드시 준수할 것, 또는 그 특정 타입 매개 변수와 결합 타입이 반드시 똑같을 것을 요구할 수 있게 합니다. 일반화 where 절은 where 키워드 시작한, 뒤에 결합 타입의 구속 조건 또는 타입과 결합 타입 사이의 같음 관계를 붙입니다. 일반화 where 절은 타입이나 함수 본문을 여는 중괄호 바로 앞에 작성합니다.

아래 예제는 allItemsMatch 라는 일반화 함수를 정의하는데, 이는 두 개의 Container 인스턴스가 동일 항목을 동일 순서로 담고 있는 지를 검사합니다. 함수는 모든 항목이 일치하면 true 라는 불리언 값을 그렇지 않으면 false 라는 값을 반환합니다.

검사할 두 컨테이너가 동일한 타입의 컨터이너는 아니어도 되지만 (물론 그래도 되긴 하지만), 동일한 타입의 항목을 쥐고 있어야 합니다. 이런 필수 조건은 타입 구속 조건과 일반화 where 절의 조합을 통해 표현합니다:

func allItemsMatch<C1: Container, C2: Container>
  (_ someContainer: C1, _ anotherContainer: C2) -> Bool
  where C1.Item == C2.Item, C1.Item: Equatable {

  // 컨테이너 둘이 동일한 수의 항목을 담고 있는지 검사함
  if someContainer.count != anotherContainer.count {
    return false
  }

  // 각각의 항목을 서로 검사하여 이들을 같다고 할 수 있는지 확인함
  for i in 0..<someContainer.count {
    if someContainer[i] != anotherContainer[i] {
      return false
    }
  }

  // 모든 항목이 일치하므로, 참을 반환함
  return true
}

이 함수는 someContaineranotherContainer 라는 두 인자를 취합니다. someContainer 인자의 타입은 C1 이고, anotherContainer 인자의 타입은 C2 입니다. C1C2 둘 다 두 컨테이너 타입의 타입 매개 변수라서 함수 호출 때 결정됩니다.

함수의 두 타입 매개 변수에 다음의 필수 조건들이 놓여져 있습니다:

첫 번째와 두 번째 필수 조건은 함수의 타입 매개 변수 목록에서 정의하며, 세 번째와 네 번째 필수 조건은 함수의 일반화 where 절에서 정의합니다.

이 필수 조건들의 의미는 이렇습니다:

세 번째와 네 번째 필수 조건을 조합하면 anotherContainer 의 항목 도 (also) != 연산자로 검사할 수 있다는 의미가 되는데, 이들은 someContainer 의 항목과 정확히 동일한 타입이기 때문입니다.

이 필수 조건들은, 두 컨테이너가 서로 다른 타입인 경우에도, 이들을 allItemsMatch(_:_:) 함수로 비교할 수 있게 합니다.

allItemsMatch(_:_:) 함수의 시작은 컨테이너 둘이 동일한 수의 항목을 담고 있는지 검사하는 겁니다. 서로 다른 수의 항목을 담고 있다면, 이들이 일치할 방법이 없으므로, 함수는 false 를 반환합니다.

이 검사 후에, 함수는 for-in 반복문 및 반-열린 범위 연산자 (..<)19someContainer 의 모든 항목을 반복합니다. 각 항목마다, 함수는 someContainer 의 항목과 anotherContainer 의 해당 항목이 같지 않은 지를 검사합니다. 두 항목이 같지 않으면, 두 컨테이너는 일치하지 않으며, 함수는 false 를 반환합니다.

불일치 (mismatch) 없이 반복문을 종료하면, 두 컨테이너가 일치하며, 함수가 true 를 반환합니다.

allItemsMatch(_:_:) 함수의 실제 사례를 보면 이렇습니다:

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

var arrayOfStrings = ["uno", "dos", "tres"]

if allItemsMatch(stackOfStrings, arrayOfStrings) {
  print("All items match.")
} else {
  print("Not all items match.")
}

// "All items match." 를 인쇄함

위 예제는 String 값을 저장하는 Stack 인스턴스를 생성하고, 세 개의 문자열을 스택에 밀어 넣습니다. 예제는 Array 인스턴스도 생성하는데 스택과 동일한 세 문자열을 담은 배열 글자 값20 으로 초기화합니다. 스택과 배열은 서로 다른 타입일지라도, 둘 다 Container 프로토콜을 준수하며, 둘에 담긴 값의 타입이 동일합니다. 그리하여 이 두 인자를 자신의 인자로 allItemsMatch(_:_:) 함수를 호출할 수 있습니다. 위 예제에서, allItemsMatch(_:_:) 함수는 두 컨테이터의 모든 항목이 일치한다고 올바로 보고합니다.

Extensions with a Generic Where Clause (일반화 where 절이 있는 익스텐션)

일반화 where 절을 익스텐션에서 사용할 수도 있습니다. 아래 예제는 이전 예제의 일반화 Stack 구조체를 확장하여 isTop(_:) 메소드를 추가합니다.

extension Stack where Element: Equatable {
  func isTop(_ item: Element) -> Bool {
    guard let topItem = items.last else {
      return false
    }
    return topItem == item
  }
}

이 새로운 isTop(_:) 메소드는 먼저 스택이 비었는지 검사한 다음, 주어진 항목과 스택의 가장 윗 항목을 비교합니다. 일반화 where 절 없이 이를 하려 하면, 문제가 있을건데: isTop(_:) 구현은 == 연산자를 사용하지만, Stack 정의는 자신의 항목이 같음 비교가 가능하길 요구하지 않아서, == 연산자의 사용이 컴파일-시간 에러가 되버린다는 겁니다. 일반화 where 절의 사용은 익스텐션에 새로운 필수 조건을 추가하게 해줘서, 스택 항목이 비교 가능할 때만 익스텐션이 isTop(_:) 메소드를 추가합니다.

isTop(_:) 메소드의 실제 사례를 보면 이렇습니다:

if stackOfStrings.isTop("tres") {
  print("Top element is tres.")
} else {
  print("Top element is something else.")
}
// "Top element is tres." 를 인쇄함

비교 가능한 원소가 아닌 스택에 isTop(_:) 메소드를 호출하려 하면, 컴파일-시간 에러를 가질 겁니다:

struct NotEquatable { }
var notEquatableStack = Stack<NotEquatable>()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue)  // 에러

프로토콜의 익스텐션에 일반화 where 절을 사용할 수 있습니다. 아래 예제는 이전 예제의 Container 프로토콜을 확장하여 startWith(_:) 메소드를 추가합니다.

extension Container where Item: Equatable {
  func startsWith(_ item: Item) -> Bool {
    return count >= 1 && self[0] == item
  }
}

startsWith(_:) 메소드는 먼저 컨테이너에 적어도 하나의 항목을 있는지 확실히 한 다음, 컨테이너의 첫 번째 항목과 주어진 항목이 일치하는 지를 검사합니다. 이 새로운 startsWith(_:) 메소드는, 컨테이터 항목이 비교 가능한 한, 위에서 사용한 스택과 배열을 포함하여, Container 프로토콜을 준수한 어떤 타입과도 사용할 수 있습니다.

if [9, 9, 9].startsWith(42) {
  print("Starts with 42.")
} else {
  print("Starts with something else.")
}
// "Starts with something else." 를 인쇄함

위 예제의 일반화 where 절은 Item 이 프로토콜을 준수하길 요구하지만, Item 이 정해진 타입이길 요구하는 일반화 where 절도 작성할 수 있습니다. 예를 들면 다음과 같습니다:

extension Container where Item == Double {
  func average() -> Double {
    var sum = 0.0
    for index in 0..<count {
      sum += self[index]
    }
    return sum / Double(count)
  }
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// "648.9" 를 인쇄함

이 예제는 자신의 Item 타입이 Double 인 컨테이너에 average() 메소드를 추가합니다. 이는 컨테이너 항목을 반복하여 더하고, 컨테이너 횟수로 나누어 평균을 계산합니다. 횟수를 명시적으로 Int 에서 Double 로 변환하여 부동-소수점 나누기가 가능하게 합니다.

익스텐션의 일부인 일반화 where 절은, 그냥 다른 곳에서 작성한 일반화 where 절이 그런 것 같이, 여러 개의 필수 조건을 포함할 수 있습니다. 목록에서 각각의 필수 조건은 쉼표로 구분합니다.

Contextual Where Clauses (상황별 where 절)

이미 일반화 타입인 상황에서 작업할 때는, 자기 자신의 일반화 타입 구속 조건이 없는 선언에서 일반화 where 절을 작성할 수 있습니다. 예를 들어, 일반화 타입의 첨자에서 또는 일반화 타입의 익스텐션에 있는 메소드에서 일반화 where 절을 작성할 수 있습니다. 아래 예제에서, Container 구조체는 일반화 (구조체) 이고, where 절은 무슨 타입 구속 조건을 만족해야 이 새로운 메소드를 컨테이너에서 사용할 수 있는 지 지정합니다.

extension Container {
  func average() -> Double where Item == Int {
    var sum = 0.0
    for index in 0..<count {
      sum += Double(self[index])
    }
    return sum / Double(count)
  }
  func endsWith(_ item: Item) -> Bool where Item: Equatable {
    return count >= 1 && self[count-1] == item
  }
}
let numbers = [1260, 1200, 98, 37]
print(numbers.average())
// "648.75" 를 인쇄함
print(numbers.endsWith(37))
// "true" 를 인쇄함

이 예제는 항목이 정수일 땐 Containeraverage() 메소드를 추가하고, 항목이 비교 가능할 땐 endsWith(_:) 메소드를 추가합니다. 함수 둘 다 일반화 where 절을 포함하여 원본 Container 선언의 일반화 Item 타입 매개 변수에 타입 구속 조건을 추가합니다.

상황별 where 절의 사용 없이 이 코드를 작성하고 싶으면, 각각의 일반화 where 절마다 하나씩, 두 개의 익스텐션을 작성합니다. 위 예제와 아래 예제의 동작은 똑같습니다.

extension Container where Item == Int {
  func average() -> Double {
    var sum = 0.0
    for index in 0..<count {
      sum += Double(self[index])
    }
    return sum / Double(count)
  }
}
extension Container where Item: Equatable {
  func endsWith(_ item: Item) -> Bool {
    return count >= 1 && self[count-1] == item
  }
}

상황별 where 절을 사용한 이 예제 버전에서, average()endsWith(_:) 구현은 둘 다 동일한 익스텐션 안에 있는데 이는 각 메소드의 일반화 where 절이 그 메소드를 사용하려면 만족해야 할 필수 조건을 알려주기 때문입니다. 이러한 필수 조건을 익스텐션의 일반화 where 절로 옮기면 동일한 상황에서 메소드를 사용하게 하지만, 필수 조건마다 하나의 익스텐션을 요구합니다.

Associated Types with a Generic Where Clause (일반화 where 절이 있는 결합 타입)

결합 타입은 일반화 where 절을 포함할 수 있습니다. 예를 들어 반복자21 를 포함한, 표준 라이브러리의 Sequence 프로토콜이 사용하는 것 같은, 버전의 Container 를 만들고 싶다고 가정합니다. 그걸 작성하는 방법은 이렇습니다:

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

  associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
  func makeIterator() -> Iterator
}

Iterator 의 일반화 where 절은 반복자가, 반복자 타입에는 상관없이, 반드시 컨테이너 항목과 동일한 항목 타입의 원소 사이를 오고 가길 요구합니다. makeIterator() 함수는 컨테이너의 반복자에 대한 접근을 제공합니다.

또 다른 프로토콜을 상속한 프로토콜에서, 상속한 결합 타입에 구속 조건을 추가하려면 프로토콜 선언에 일반화 where 절을 포함하면 됩니다. 예를 들어, 다음 코드가 선언한 ComparableContianer 프로토콜은 ItemComparable 을 준수하길 요구합니다:

protocol ComparableContainer: Container where Item: Comparable { }

Generic Subscripts (일반화 첨자)

첨자는 일반화일 수 있으며, 일반화 where 절을 포함할 수 있습니다. 자리 표시용 타입 이름은 subscript 뒤의 꺾쇠 괄호 안에 작성하며, 일반화 where 절은 첨자 본문을 여는 중괄호 바로 앞에 작성합니다. 예를 들면 다음과 같습니다:

extension Container {
  subscript<Indices: Sequence>(indices: Indices) -> [Item]
    where Indices.Iterator.Element == Int {
      var result = [Item]()
      for index in indices {
        result.append(self[index])
      }
      return result
  }
}

Container 프로토콜 익스텐션이 추가한 첨자는 일렬로 나열된 색인을 취해서 각각의 주어진 색인 위치의 항목을 담은 배열을 반환합니다. 이 일반화 첨자의 구속 조건은 다음과 같습니다:

함께 취합해보면, 이 구속 조건들은 indices 매개 변수에 전달한 값은 일렬로 나열된 정수들이라는 걸 의미합니다.

다음 장

Opaque Types (불투명 타입) >

참고 자료

  1. 원문은 Generics에서 확인할 수 있습니다. 

  2. ‘언어 설명서 (Language Guide)’ 는 ‘스위프트 프로그래밍 언어 (Swift Programming Language)’ 책에서 스위프트 문법을 설명하는 부분으로, 지금 보고 있는 문서가 바로 ‘언어 설명서 (Language Guide)’ 에 해당합니다. 책의 전체 목록은 Swift 5.7: Swift Programming Language (스위프트 프로그래밍 언어) 에서 확인할 수 있습니다. 

  3. ‘타입 보조 설명 (type annotation)’ 은 let a: Int 에서의 Int 처럼 해당 속성의 타입을 설명하는 것입니다. ‘타입 보조 설명’ 에 대한 더 자세한 정보는 The Basics (기초) 장의 Type Annotations (타입 보조 설명) 부분을 보도록 합니다. 

  4. ‘낙타 모양 대문자 (upper camel case)’ 는 스위프트에서 사용하는 ‘타입 이름’ 이 ‘낙타 등 모양’ 처럼 생겼기 때문에 붙은 이름입니다. ‘낙타 모양 대문자’ 에 대한 더 자세한 설명은, Structures and Classes (구조체와 클래스) 장의 Definition Syntax (정의 구문) 부분을 보도록 합니다. 

  5. ‘순서 있는 집합 (ordered set)’ 은 ‘정렬된 집합 (sorted set)’ 과 수학적인 의미가 다릅니다. 이 둘의 차이점에 대해서는, ‘StackOverflow’ 의 What is the difference between an ordered and a sorted collection? 항목을 보도록 합니다. 

  6. Array 보다 Stack 이 더 많이 제약됐다는 건 Stack 이 되려면 Array 보다 더 많은 조건이 필요하다는 의미입니다. 

  7. ‘후입 선출법 (last in, first out)’ 이라는 용어에 대해서는 위키피디아의 FIFO and LIFO accounting선입 선출법과 후입 선출법 항목을 보도록 합니다. 이 용어는 컴퓨터 이전부터 사용되었던 것 같습니다. 컴퓨터 용어로 ‘LIFO’ 를 검색하면 ‘스택 (stack)’ 으로 연결되는데, 이에 대한 더 자세한 정보는, 위키피디아의 Stack (abstract data type) 항목과 스택 항목을 보도록 합니다. 

  8. 쉽게 말해서, ‘일반화 (generic) 타입’ 에 대한 ‘익스텐션’ 을 정의할 때는, 꺽쇠 괄호 부분을 작성하지 않는다는 의미입니다. 

  9. ‘프로토콜 합성 (protocol composition)’ 에 대한 더 자세한 정보는, Protocols (프로토콜; 규약) 장에 있는 Protocol Composition (프로토콜 합성) 부분을 보도록 합니다. 

  10. ‘해시 가능 (hashable)’ 은, 본문 바로 뒤에 설명하듯, 자신을 유일하게 나타낼 방법을 제공할 수 있다는 의미입니다. 해시 가능에 대한 더 자세한 정보는, Collection Types (집합체 타입) 장에 있는 Hash Values for Set Types (셋 타입의 해시 값) 부분을 참고하기 바랍니다. 

  11. 원문 자체가 애플 개발자 문서의 링크입니다. 

  12. ‘일반화 프로그래밍 (genenric programming)’ 에 대한 더 자세한 정보는, 위키피디아의 Generic programming 항목을 보도록 합니다. 

  13. ‘타입을, 자신의 고정 타입 보단, 자신의 개념적인 성질로 성격짓는다’ 는 건 프로그래밍 분야에서 ‘덕 타이핑 (duck typing)’ 과 비슷한 개념입니다. ‘덕 타이핑 (duck typing)’ 에 대한 더 자세한 정보는, 위키피디아의 Duck typing 항목과 덕 타이핑 항목을 보도록 합니다. 

  14. ‘자리 표시용 이름 (placeholder name)’ 에 대해서는 위에 있는 Generic Functions (일반화 함수)) 부분을 보도록 합니다. 

  15. ‘결합 타입 (associated types)’ 은 프로토콜에서 ‘일반화 (generic)’ 를 구현하는 방법입니다. 프로토콜 그 자체는 타입 매개 변수를 사용할 수 없으므로, 결합 타입을 써서 일반화를 하는 것입니다. 즉, 클래스라면 class Container<Item> { ... } 이라고 하는데, 프로토콜은 그럴 수 없으므로 protocol Container { associatedtype Item; ... } 라고 하는 것입니다. 

  16. 초창기 스위프트 문법에선 프로그래머가 수동으로 typealias Item = Int 라고 해줘야 했었습니다. 하지만, 최근에는 append(_ item:) 함수를 구현할 때 item: 타입이 Int 라고 하면 스위프트가 알아서 ItemInt 라는 걸 추론하기 때문에 typealias Item = Int 를 따로 작성할 필요가 없습니다. 

  17. 예제 코드의 associatedtype Item: Equatable 부분입니다. 

  18. 이 필수 조건은 항목의 값뿐만 아니라, 타입도 똑같다는 걸 보장하는데, == 연산자로 비교하려면 항목의 타입이 서로 똑같아야 하기 때문입니다. 

  19. ‘반-열린 범위 연산자 (half-open range operator)’ 에 대한 더 자세한 내용은, Basic Operators (기초 연산자) 장의 Half-Open Range Operator (반-열린 범위 연산자) 부분을 보도록 합니다. 

  20. ‘글자 값 (literal)’ 에 대해서는, Lexical Structure (어휘 구조) 장의 Literals (글자 값) 부분을 보도록 합니다. 

  21. 컴퓨터 프로그래밍 용어로 ‘반복자 (iterator)’ 는 컨테이너의 항목 사이를 오갈 수 있게 만드는 객체를 의미합니다. 반복자에 대한 더 자세한 정보는, 위키피디아의 Iterator 항목과 반복자 항목을 보도록 합니다. 

  22. 본문에서 말하는 ‘일반화 매개 변수 (generic parameter)’ 는 ‘타입 매개 변수 (type parameter)’ 를 의미합니다.