You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generic(제네릭) 코드를 사용하면 정의한 요구 사항에 따라 모든 타입에서 작동할 수 있는
유연하고 재사용 가능한 함수와 타입을 작성할 수 있다
이는 중복을 피하고 명확하고 추상적인 방식으로 코드를 작성할 수 있다
제네릭은 Swift의 가장 강력한 기능 중 하니이며, Swift 표준 라이브러리의 대부분은 제네릭 코드로 빌드된다
사실 지금까지 작성한 모든 글에서 제네릭을 사용하고 있었다
예를 들어 Swift의 배열, 딕셔너리 타입은 모두 제네릭 컬렉션이다
즉, 배열에 Int, String등 모든 타입을 저장할 수 있는 이유가 제네릭 타입이기 때문에 저장되는 타입에 제한이 없다
제네릭이 해결할 문제
다음 코드는 제네릭을 사용하지 않은 함수 swapTwoInts(_: _: )를 정의한 것이다
func swapTwoInts(_ a:inoutInt, _ b:inoutInt){lettemporaryA= a
a = b
b = temporaryA
}
swapTwoInts(::) 함수는 In-Out 매개변수를 사용하여 원래 값인 b를 a로, 원래 값인 a를 b로 바꾼다
함수를 호출하여 두 Int 변수의 값을 바꿀 수 있다
varsomeInt=3varanotherInt=107swapTwoInts(&someInt,&anotherInt)print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"
swapTwoInts는 Int 값에만 적용할 수 있다는 한계가 있다
만약 String 값 또는 Double 값을 바꾸고 싶다면 새로 함수를 작성해야 한다
func swapTwoStrings(_ a:inoutString, _ b:inoutString){lettemporaryA= a
a = b
b = temporaryA
}func swapTwoDoubles(_ a:inoutDouble, _ b:inoutDouble){lettemporaryA= a
a = b
b = temporaryA
}
swapTwoInts(_: ), swapTwoStrings(_: ) 및 swapTwoDoubles(_: _: ) 함수의 본문이 동일하다는 것을 알 수 있다
유일한 차이점은 허용되는 값의 타입(Int, String 및 Double)이다
모든 타입의 두 값을 교환하는 단일 함수를 작성하는 것이 더 유용하고 훨씬 유연하다
제네릭 코드를 사용하면 이러한 함수를 작성할 수 있다
즉, 동일한 동작을 하는 코드를 다양한 타입에서 사용하기 위해선 제네릭 코드를 사용하면 된다
세 함수 모두에서 a와 b의 타입은 동일해야 한다
a와 b의 타입이 동일하지 않으면 값을 교환할 수 없다
Swift는 type-safe 언어이며 String 타입의 변수와 Double 타입의 변수가 서로 값을 교환할 수 있도록 허용하지 않는다
이런 시도는 컴파일 오류를 발생시킨다
제네릭 함수(Generic Functions)
제네릭 함수는 모든 타입과 함께 사용할 수 있다
다음은 위에서 가져온 swapTwoInts(_: _: ) 함수의 제네릭을 활용한 swapTwoValues(_: _: ) 함수다
func swapTwoValues<T>(_ a:inoutT, _ b:inoutT){lettemporaryA= a
a = b
b = temporaryA
}
swapTwoValues(_: _: ) 함수의 본문은 swapTwoInts(_: _: ) 함수의 본문과 동일하다
그러나, swapTwoValues(_: )의 첫 번째 줄은 swapTwoInts(_: )와 약간 다르다
첫 번째 줄은 다음과 같이 비교된다
함수를 제네릭으로 만들면 타입 이름 대신(ex. Int, Double.. ) 를 사용한다
이는 placeholder 타입이라고 하며 개발자는 T가 무슨 타입인지 정하진 않았지만
a,b는 모두 같은 타입 T라는 것은 선언한 것으로
T 대신 사용할 실제 타입은 swapTwoValues(_: _: ) 함수를 호출할 때마다 결정된다
제네릭 함수와 기존 함수와의 차이점은 제네릭 함수는 정의할 때 라는 것을 적어준다
여기서 꺾은 괄호는 swapTwoValues(_: )에서 함수 이름과는 상관없이 정의 내 placeholder 타입의 이름이라는 걸 알리는 용도이며
T는 placeholder 타입이기 때문에 Swift는 T라는 실제 타입을 찾지 않는다
swapTwoValues(_: _: ) 함수는 이제 swapTwoInts와 동일한 방식으로 호출할 수 있다
두 값이 서로 동일한 타입이면 모든 타입의 두 값을 전달할 수 있다
swapTwoValues(_: _: )가 호출될 때마다 T에 사용할 타입은 함수에 전달된 값의 타입으로 추론된다
아래의 두 예시에서, T는 각각 Int와 String으로 추론된다
varsomeInt=3varanotherInt=107swapTwoValues(&someInt,&anotherInt)
// someInt is now 107, and anotherInt is now 3
varsomeString="hello"varanotherString="world"swapTwoValues(&someString,&anotherString)
// someString is now "world", and anotherString is now "hello"
위에서 정의한 swapTwoValues(_: _: ) 함수는 Swift 표준 라이브러리의 일부인 swap이라는 제네릭 함수에 영감을 받아 구성한 함수다
자체 코드에서 swapTwoValues(_: _: ) 함수의 동작이 필요한 경우, 자체 구현을 제공하는 대신 Swift의 기존 swap(_: _: ) 함수를 사용할 수 있다
Type Parameters
위의 swapTwoValues(_: _: ) 예제에서 placeholder 타입 T는 타입 매개 변수의 예시이다
타입 매개 변수는 placeholder 타입을 지정하고 이름을 지정하며,
함수 이름 바로 뒤에 일치하는 한 쌍의 꺾은 괄호(예: ) 사이에 기록됩니다
즉, 여기서 지정한 타입 매개 변수의 이름은 T아다. 만약 <B>라고 함수 뒤에 썼다면 타입 매개 변수의 이름은 B다
타입 매개 변수를 지정한 후에는 함수 매개 변수의 타입(swapTwoValues(_: _: ) 함수의 a 및 b 매개 변수 등)을 정의하거나
함수의 반환 타입 또는 함수 본문 내의 타입 주석으로 정의할 수 있다
각각의 경우, 타입 파라미터는 함수가 호출될 때마다 실제 타입으로 대체된다
위의 swapTwoValues(_: _: ) 예제에서는 함수를 처음 호출할 때 T가 Int로 대체되고, 두 번째 호출할 때 String으로 대체된다
만약 타입 매개변수가 여러 개 필요하다면 <T, B>와 같이
여러 타입 매개 변수 이름을 꺾은 괄호 안에 쉼표로 구분하여 작성하여 둘 이상의 타입 매개 변수를 제공할 수 있다
Naming Type Parameters
대부분의 경우 타입 매개 변수는 설명이 포함된 이름을 가지고 있다
이는 타입 매개 변수와 해당 매개 변수가 사용되는 제네릭 타입 또는 함수 사이의 관계를 독자에게 알려준다
ex. Dictionary의 <Key, Value>과 같이 이름을 만든 것도 확인 가능
그러나, 이들 사이에 유의미한 관계가 없는 경우에는 처럼 단일 문자를 사용하는 것이 일반적이다
항상 타입 매개 변수의 대문자 이름(예: T 및 MyType 매개 변수)을 지정하여 값이 아닌 placeholder 타입임을 나타낸다
제네릭 타입(Generic Types)
제네릭 함수 외에도 Swift를 사용하면 자신만의 제네릭 타입을 정의할 수 있다
이러한 커스텀 클래스, 구조체 및 열거형은 배열 및 딕셔너리와 유사한 방식으로 모든 타입과 함께 사용할 수 있다
이번 섹션에서는 Stack이라는 일반 컬렉션 타입을 작성하는 방법을 보여준다
Stack은 Array와 비슷하지만 보다 제한된 LIFO(Last In First Out)의 규칙을 따르는 정렬된 값의 집합이다
따라서, 컬렉션 끝에서만 항목을 추가하고 제거할 수 있게 되며 값을 추가하는 것은 PUSH, 값을 제거하는 것은 POP으로 불린다
스택의 개념은 navigation 계층의 뷰 컨트롤러를 모델링하는 데 UINavigationController 클래스에서 사용된다
navigation 스택에 뷰 컨트롤러를 PUSH하려면 UINavigationController 클래스 pushViewController(_:animated:) 메서드를 호출하고
navigation 스택에서 뷰 컨트롤러를 POP하려면 popViewControllerAnimated(_:) 메서드를 호출한다
스택은 컬렉션을 관리하기 위해 엄격한 "LIFO(Last In First Out)" 접근 방식이 필요할 때마다 유용한 컬렉션 모델
아래 그림은 스택의 PUSH와 POP을 나타낸 그림이다. 왼쪽부터 설명을 하면 다음과 같다
Stack의 제네릭 버전은 이전 버전과 동일하지만 실제 타입의 Int가 아닌 Element라는 타입 매개 변수를 사용한다
때문에 구조체 안에서 타입의 이름으로 Element를 사용하는 것을 볼 수 있다
타입 매개 변수는 구조체 이름 바로 뒤에 있는 한 쌍의 꺾은 괄호() 안에 기록된다
Element는 나중에 제공할 타입에 대한 placeholder 타입 이름을 정의한다
나중에 제공되는 타입인 Element는 구조체 정의 안에서 사용되며, 다음 세 위치에서 placeholder로 사용된다
Element 타입은 처음부터 타입의 종류가 정해진 것이 아니고 사용될 때 정해진다
items 프로퍼티를 초기화할 때 Element 타입으로 만든다
push(_ :) 메서드에서 사용하는 매개변수의 타입은 Element다
pop() 메서드에서 반환하는 값의 타입은 Element다
제네릭 타입이기 때문에 Stack을 사용하여 배열, 딕셔너리와 유사한 방식으로 Stack을 만들 수 있다
스택에 저장할 타입을 꺾은 괄호 안에 입력하여 새 스택 인스턴스를 만든다
ex. 문자열의 새 스택을 만들려면 Stack()을 작성
varstackOfStrings=Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
위의 코드를 실행하면 그림과 같은 방식으로 실행된다
스택에 아래 값을 입력하면 상위 값인 "cuatro"가 제거되고 반환된다
fromTheTop에는 "cuatro"가 들어있게 되고 스택에는 3개의 값만 남는다
letfromTheTop= stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
Extending a Generic Type
제네릭 타입에 익스텐션을 사용할 때 타입 매개변수는 제공하지 않아도 된다
하지만, 원본 제네릭 타입을 정의할 때 정의한 타입 매개변수 목록은 익스텐션 본문 안에서 사용할 수 있으며
타입 매개변수들의 이름도 그대로 사용할 수 있다
다음 예제에서는 제네릭 스택 타입을 익스텐션하여 topItem이라는 읽기 전용 계산 프로퍼티를 추가한다
해당 프로퍼티는 스택에서 최상위 item을 POP 없이 스택에서 반환한다
이 글에서 구현한 swapTwoValues,Stack은 모든 타입에서 사용될 수 있었다
하지만, 제네릭 함수와 타입에서 사용할 수 있는 타입에 제약 조건을 주는 것이 유용한 경우가 있다
타입 제약조건을 만족하기 위해선 타입 매개 변수가 특정 클래스를 상속하거나 특정 프로토콜을 준수해야 한다
예를 들어 Swift의 Dictionary는 Key로 사용할 수 있는 타입에 제약을 뒀다
Dictionary의 Key에 사용될 수 있는 타입은 반드시 hashable이어야 한다
즉, 고유하게 표현할 수 있는 id와 같은 방법을 제공해야 한다
Dictionary에는 특정 키에 대한 값이 이미 포함되어 있는지 확인할 수 있어야하고
이러한 제약조건이 없으면 Dictionary는 특정 키에 대한 값을 삽입할지 바꿀지 알 수 없으며
이미 Dictionary에 있는 특정 키에 대한 값을 찾을 수도 없다
이러한 제약조건은 Dictionary의 Key 타입에 대한 타입 제약조건에 의해 생기며
제약조건을 만족하기 위해선 Hashable 프로토콜을 만족하는 타입을 사용해야 한다
Swift의 기본적인 타입인 String, Int, Double 등은 Hashable 프로토콜을 준수하므로 Dictionary에서 사용할 수 있다
직접 제네릭 타입을 만들 때 고유한 제약 조건을 정의할 수 있으며, 이러한 제약조건은 제네릭 프로그래밍의 많은 기능을 제공할 수 있다
Hashable과 같은 추상적 개념은 구체적인 타입이 아닌 개념적 특성 측면에서 타입을 특성화한다
1) Type Constraint 문법
타입 매개 변수를 지정할 때 :으로 구분된 타입 매개 변수 이름 뒤에 단일 클래스나 프로토콜 제약 조건을 작성하여 제약조건을 만들 수 있다
func someFunction<T:SomeClass, U:SomeProtocol>(someT:T, someU:U){
// function body goes here
}
예를 들어 위의 코드에는 두 개의 타입 매개 변수가 있다
T는 SomeClass 클래스의 서브 클래스여야 하고
U는 SomeProtocol 프로토콜을 준수하는 타입 이어야 한다
위와 같이 타입 매개 변수에 제약조건을 만들 수 있다
2) Type Constraint 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
}}returnnil}
findIndex(ofString:in:) 함수를 사용하여 문자열 배열에서 문자열 값의 인덱스를 찾을 수 있다
letstrings=["cat","dog","llama","parakeet","terrapin"]iflet foundIndex =findIndex(ofString:"llama", in: strings){print("The index of llama is \(foundIndex)")}
// Prints "The index of llama is 2"
배열에서 값의 인덱스를 찾는 원리가 문자열에만 유용한 것은 아니다
하지만, 위의 함수는 String 타입에만 사용할 수 있으므로 모든 타입에 사용할 수 있는 제네릭 함수로 바꿔보자
문자열에 대한 언급을 T 타입의 값으로 대체하여 기존의 함수와 동일한 기능을 쓸 수 있다
다음은 findIndex(of:in:)라는 제네릭 버전의 findIndex(String:in:)가 작성되는 방법이다
함수는 배열에서 옵셔널한 값이 아닌 옵셔널한 인덱스 번호를 반환하므로 함수의 반환 타입은 여전히 Int?이다
단, 해당 함수는 컴파일되지 않으므로 주의해야 한다
func findIndex<T>(of valueToFind:T, in array:[T])->Int?{for(index, value)in array.enumerated(){if value == valueToFind {return index
}}returnnil}
함수는 위에 작성한 것처럼 컴파일되지 않는데, 문제는 동일성 검사 "if value == valueToFind"에 있다
Swift의 모든 타입을 연산자(==)와 비교할 수 있는 것은 아니다
예를 들어, 복잡한 데이터 모델을 나타내는 고유한 클래스나 구조를 만든다면
해당 클래스나 구조에 대한 "동일"의 의미는 Swift가 추측할 수 있는 것이 아니게 된다
때문에 가능한 모든 타입 T에서 해당 코드가 작동한다는 것을 보장할 수 없으며, 코드를 컴파일하려고 할 때 오류가 발생한다
그러나, 완전히 불가능한 건 아니다
Swift 표준 라이브러리는 Equatable이라고 불리는 프로토콜을 정의하는데,
해당 프로토콜을 따르는 타입의 값을 비교하기 위한 비교 연산자인 (==)와 (!==)을 구현할 수 있도록 하며
Swift의 모든 표준 타입은 자동으로 Equatable 프로토콜을 지원한다
equatable인 모든 타입은 equal to 연산자를 사용할 수 있으므로
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
}}returnnil}
findIndex(of:in:)의 단일 타입 매개 변수는 T: Equatable로 작성되며, 이는 "Equatable 프로토콜에 부합하는 모든 타입"을 의미한다
이제 findIndex(of:in:) 함수가 성공적으로 컴파일되고, Double 또는 String과 같이 동등하게 사용할 수 있다
letdoubleIndex=findIndex(of:9.3, in:[3.14159,0.1,0.25])
// doubleIndex is an optional Int with no value, because 9.3 isn't in the array
letstringIndex=findIndex(of:"Andrea", in:["Mike","Malcolm","Andrea"])
// stringIndex is an optional Int containing a value of 2
Associated Types
프로토콜을 정의할 때 정의의 일부로 하나 이상의 associated 타입을 선언하는 경우가 유용할 수 있다
associated 타입은 프로토콜의 일부로 사용되는 타입에 placeHolder 타입 이름을 제공한다
프로토콜에서 이러한 타입을 정의할 땐 associatedtype 키워드를 함께 사용해줘야 한다
1) Associated Types in Action
다음은 Item이라는 Associated Type을 선언하는 Container 프로토콜의 예시이다
Int 인덱스 값을 사용하는 서브 스크립트: 컨테이너의 각 item을 검색할 수 있어야 한다
프로토콜은 컨테이너의 item을 저장하는 방법이나 허용되는 타입을 지정하지 않는다
프로토콜은 컨테이너로 간주되기 위해 모든 타입이 제공해야 하는 3가지의 기능만 지정한다
적합한 타입은 세 가지 요건을 충족하는 한 추가 기능을 제공할 수 있다
Container 프로토콜을 준수하는 모든 타입은 저장되는 값의 타입을 지정할 수 있어야 한다
특히 올바른 타입의 item만 컨테이너에 추가되도록 해야 하며, 서브 스크립트가 반환하는 item의 타입에 대해 명확해야 한다
이러한 요구 사항을 정의하기 위해 Container 프로토콜은 특정 컨테이너에 대해
해당 타입이 무엇인지 알지 못한 채 컨테이너가 보유할 요소의 타입을 참조하는 방법이 필요하다
Container 프로토콜은 append(_:) 메서드에 전달된 값이 컨테이너의 Item 타입과 동일해야 하며
컨테이너의 서브 스크립트가 반환하는 값이 컨테이너의 Item 타입과 동일하도록 지정해야 한다
이를 위해 Container 프로토콜은 Item이라는 관련 타입을 선언하고 associatedtype Item으로 작성한다
프로토콜은 Item이 무엇인지는 정의하지 않는다
해당 정보는 적합한 타입이 차후에 제공할 수 있도록 남겨지고, 그럼에도 불구하고 Item alias는 컨테이너의 Item 타입을 참조하고
append(_ :) 메서드 및 서브 스크립트와 함께 사용할 타입을 정의하여, 컨테이너에서 예상 동작이 적용되도록 하는 방법을 제공한다
다음은 Container 프로토콜을 준수하는 제네릭을 사용하지 않은 IntStack 구조체이다
위의 IntStack 코드는 Container 프로토콜을 준수하는 제네릭을 사용하지 않은 구조체이다
IntStack 구조체는 프로토콜에서 요구한 Item 타입의 실제 타입으로 Int를 사용하게 된다
IntStack 타입의 기존 기능의 일부를 랩핑하여 이러한 요구 사항을 충족한다
IntStack은 Container 프로토콜의 구현을 위해, 사용할 적절한 Item이 Int의 한 타입이라고 명시한다
typealias Item = Int의 정의는 Container 프로토콜 구현을 위해 Item의 추상 타입을 Int의 구체적인 타입으로 바꾼다
Swift의 타입 추론 덕분에 IntStack 정의의 일부로 구체적인 Int의 Item를 선언할 필요가 없다
Swift는 append(_:) 메서드의 item 매개 변수 타입과 서브 스크립트의 반환 타입을 보는 것만으로 사용할 적절한 item을 추론하며,
실제로 위의 코드에서 Item = Int 라는 부분을 삭제해도 Item에 사용해야 하는 타입이 명확하기 때문에 모든 것이 여전히 작동한다
하지만, 이렇게 구현하면 다른 타입에 대해서는 사용할 수 없기 때문에 제네릭을 사용하여 다시 구현해보자
위와 같이 제네릭을 사용한 Stack 구조체를 정의할 수 있다
여기서는 타입 매개 변수로 Element 라는 이름을 사용했다
Element가 append(_:) 메서드의 item 매개 변수의 타입과 서브 스크립트의 반환 타입으로 사용된다
따라서, Swift는 Element가 해당 특정 컨테이너의 Item으로 사용하기에 적합한 타입이라고 추론할 수 있다
이젠 Container 프로토콜을 준수하는 Stack 구조체에 다양한 타입을 사용할 수 있다
3) Extending an Existing Type to Specify an Associated Type
익스텐션을 사용하여 기존 타입에 프로토콜을 준수하도록 추가할 수 있다
여기에는 Associated 타입을 가진 프로토콜도 포함된다
아래와 같이 Array 타입에 Container 프로토콜을 추가로 채택할 수 있다
Array 타입에는 이미 append(_ :) 메서드, count 프로퍼티, Int인덱스를 사용하는 서브 스크립트가 존재하기 때문에
Container의 모든 요구 사항을 준수한다
extensionArray:Container{}
배열의 기존 append(_ :) 메서드와 서브 스크립트를 사용하면
위의 제네릭 스택 타입 예시와 마찬가지로 Swift가 항목에 사용할 적절한 타입을 추론할 수 있다
익스텐션을 정의한 후에는 모든 배열을 컨테이너로 사용할 수 있다
위와 같이 Item에 Equatable 프로토콜을 준수해야 한다는 제약조건을 추가할 수 있다
5) Using a Protocol in Its Associated Type's Constraints
프로토콜은 자체 요구 사항의 일부로 나타낼 수 있다.
예를 들어, 다음은 suffix(_:) 메서드 요구 사항을 추가하여 Container 프로토콜을 세분화하는 프로토콜이다
suffix(_:) 메서드는 컨테이너 끝에서 지정된 수의 요소를 Suffix 타입의 인스턴스 내부에 저장하여 반환한다
위의 코드는 Container 프로토콜에 요구사항을 추가한 SuffixableContainer 프로토콜을 정의한 예시다
SuffixableContainer 프로토콜 Suffix 라는 associated 타입과 suffix(_ :) 메서드를 추가로 요구한다
Suffix 타입은 SuffixableContainer 프로토콜을 준수해야 하고, 해당 Item 타입은 Container 프로토콜의 Item 타입과 동일해야 한다
Item에 대한 제약 조건은 제네릭 where 구문에 나타난다 Associated Types with a Generic Where Clause 페이지에서 관련 정보를 더 얻을 수 있다
다음은 위의 Generic Types의 Stack 타입에 SuffixableContainer 프로토콜을 추가로 채택하여 익스텐션한다
extensionStack:SuffixableContainer{func suffix(_ size:Int)->Stack{varresult=Stack()forindexin(count-size)..<count {
result.append(self[index])}return result
}
// Inferred that Suffix is Stack.
}varstackOfInts=Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)letsuffix= stackOfInts.suffix(2)
// suffix contains 20 and 30
해당 코드에서 Suffix의 associated 타입도 Stack이므로 suffix(_ :) 메서드는 다른 Stack 타입을 반환한다
또한, SuffixableContainer를 준수하는 타입은 자신과는 다른 Suffix 타입을 가질 수 있다
즉, suffix 작업은 다른 타입을 반환할 수도 있다
아래의 코드는 제네릭을 사용하지 않은 IntStack 구조체에 익스텐션을 사용하여
SuffixableContainer 프로토콜을 추가로 채택하는 코드다
Suffix의 타입으로 IntStack대신 Stack를 사용한 것을 볼 수 있다
extensionIntStack:SuffixableContainer{func suffix(_ size:Int)->Stack<Int>{varresult=Stack<Int>()forindexin(count-size)..<count {
result.append(self[index])}return result
}
// Inferred that Suffix is Stack<Int>.
}
Generic Where Clauses
타입 제약 조건을 사용하면 제네릭 함수, 서브 스크립트, 타입에서
associated 타입 매개 변수에 대한 요구사항을 정의할 수 있다
Associated 타입에 대해 요구사항을 정의하는 것도 유용할 수 있다
이러한 정의는 제네릭 where 절을 정의하여 할 수있다
제네릭 where절을 사용하면 associated 타입이 특정 프로토콜을 준수해야 하거나
특정 타입 매개변수와 동일해야 한다고 요구할 수 있다
제네릭 where 절은 where키워드로 시작하고 그 뒤에 associated 타입에 대한 제약조건, 동등관계를 써주면 된다
아래 예제에서는 두 Container 인스턴스에 동일한 항목이 동일한 순서로 포함되어 있는지 확인하는
allItemsMatch라는 일반 함수를 정의한다
해당 함수는 모든 항목이 일치하면 true를 반환하고 일치하지 않으면 false 값을 반환한다
검사할 두 컨테이너가 동일한 타입의 컨테이너일 필요는 없지만, 동일한 타입의 items를 포함해야 한다
해당 요구사항은 타입 제약조건과 일반 where 절의 조합을 통해 표현된다
func allItemsMatch<C1:Container, C2:Container>(_ someContainer:C1, _ anotherContainer:C2)->Boolwhere C1.Item ==C2.Item, C1.Item:Equatable{
// Check that both containers contain the same number of items.
if someContainer.count != anotherContainer.count {returnfalse}
// Check each pair of items to see if they're equivalent.
foriin0..<someContainer.count {ifsomeContainer[i]!=anotherContainer[i]{returnfalse}}
// All items match, so return true.
returntrue}
이 함수는 someContainer와 anotherContainer라는 두 개의 인수를 사용한다
컨테이너 인수는 C1 타입 다른 컨테이너 인수는 C2 타입이다
C1과 C2는 모두 함수가 호출될 때 결정되는 두 컨테이너 타입에 대한 타입 매개변수다
함수의 두 가지 타입 매개변수에는 다음과 같은 요구사항이 있다
C1은 Container 프로토콜을 준수해야 한다 - C1: Container
C2도 Container 프로토콜을 준수해야 한다 - C2: Container
C1의 Item은 C2의 Item과 동일해야 한다 - C1.Item == C2.Item
C1의 Item은 Equatable 프로토콜을 반드시 준수해야한다 - C1.Item: Equatable
첫 번째 요건과 두 번째 요건은 함수의 타입 매개변수 목록에 정의되며, 세 번째 요건과 네 번째 요건은 함수의 제네릭 where 절에 정의된다
이러한 요구사항은 다음을 의미한다
someContainer는 C1 타입의 컨테이너다
otherContainer는 C2 타입의 컨테이너다
someContainer와 otherContainer에 동일한 타입의 items이 포함되어 있다
someContainer의 items은 부등 연산자(!=)로 서로 다른지 확인할 수 있다
세 번째와 네 번째 요구 사항은 otherContainer의 items이 someContainer의 items와 정확히 동일한 타입이기 때문에
부등 연산자(!=)를 통해 확인할 수 있음을 의미한다
이러한 요구 사항을 통해 allItemsMatch(_:) 함수가 두 컨테이너 타입이 다른 경우에도 비교할 수 있다
allItemsMatch(::) 함수은 두 컨테이너에 동일한 수의 items이 포함되어 있는지 확인하는 것으로 시작한다
서로 다른 수의 items을 포함하는 경우 일치시킬 방법이 없으며 함수는 false를 반환한다
해당 검사를 수행한 후, 함수는 for-in 루프와 반 열림 범위 연산자(..<)가 있는 일부 컨테이너의 모든 item에 걸쳐 순회한다
함수는 각 item에 대해 someContainer의 item이 otherContainer의 해당 item과 동일하지 않은지 확인한다
두 item이 동일하지 않으면 두 컨테이너가 일치하지 않고 함수가 false를 반환한다
일치하지 않는 item을 찾지 않고 루프가 완료되면 두 컨테이너가 일치하고 함수가 true를 반환한다
allItemsMatch(_:_:) 함수가 작동하는 방식은 다음과 같다
varstackOfStrings=Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")vararrayOfStrings=["uno","dos","tres"]ifallItemsMatch(stackOfStrings, arrayOfStrings){print("All items match.")}else{print("Not all items match.")}
// Prints "All items match."
위의 예제는 Stack 인스턴스를 생성하여 String 값을 저장하고 세 개의 문자열을 스택에 푸시한다
예제에서는 또한 스택과 동일한 세 개의 문자열을 포함하는 배열 리터럴로 초기화된 배열 인스턴스를 만든다
스택과 배열이 서로 다른 타입이지만 Container 프로토콜을 준수하며 둘 다 동일한 타입의 값을 포함한다
따라서, 이 두 컨테이너를 인수로 하여 allItemsMatch(_:) 함수를 호출할 수 있다
위의 예에서 allItemsMatch(_:_:) 함수는 두 컨테이너의 모든 항목이 일치한다고 올바르게 보고한다
Extensions with a Generic Where Clause
제네릭 where절을 익스텐션에서도 사용할 수 있다
아래 코드는 제네릭을 사용한 Stack 구조체에 익스텐션을 사용할 때 where절도 사용한 코드이다
익스텐션으로 추가한 isTop(_ :) 메서드는 매개변수로 받은 값이 현재 인스턴스의 제일 위에 존재하는 값인지를 확인해주는 메서드이다
여기서 타입 매개변수로 사용할 Element는 Equatable 프로토콜을 준수해야 한다. 만약 그렇지 않다면 컴파일 에러를 발생한다
isTop(:) 메서드는 먼저 스택이 비어 있지 않은지 확인한 다음 지정된 item을 스택의 맨 위 item과 비교한다
제네릭 where 절 없이 해당 작업을 수행하려고 하면 다음과 같은 문제가 발생한다
isTop(:)의 구현에서는 == 연산자를 사용하지만
Stack의 정의에서는 해당 item이 equatable할 필요가 없으므로
== 연산자를 사용하면 컴파일 타임 오류가 발생한다
제네릭 where 절을 사용하면 익스텐션에 새 요구 사항을 추가할 수 있으므로
스택의 item이 equatable한 경우에만 익스텐션에서 isTop(_:) 메서드를 추가할 수 있다
if stackOfStrings.isTop("tres"){print("Top element is tres.")}else{print("Top element is something else.")}
// Prints "Top element is tres."
요소가 equatable하지 않은 스택에서 isTop(_:) 메서드를 호출하려고 하면 컴파일 시간 오류가 발생한다
startsWith(:) 메서드는 먼저 컨테이너에 하나 이상의 item이 있는지 확인한 다음
컨테이너의 첫 번째 item이 지정된 item과 일치하는지 확인한다
새로운 startsWith(:) 메서드는 컨테이너의 item이 동일하다면
위에서 사용된 스택과 배열을 포함하여 Container 프로토콜에 부합하는 모든 타입에 사용할 수 있다
if[9,9,9].startsWith(42){print("Starts with 42.")}else{print("Starts with something else.")}
// Prints "Starts with something else."
위의 예시에서 generic where 절은 Item이 프로토콜을 준수하도록 요구하지만,
Item이 특정 타입이어야 하는 절에 generic where를 작성할 수도 있다
extensionContainerwhere Item ==Double{func average()->Double{varsum=0.0forindexin0..<count {
sum +=self[index]}return sum / Double(count)}}print([1260.0,1200.0,98.6,37.0].average())
// Prints "648.9"
예시에서는 Item 타입이 Double인 컨테이너에 average() 메서드를 추가한다
컨테이너의 item을 순회하여 합산하고, 컨테이너의 개수로 나누어 평균을 계산한다
부동 소수점 나눗셈을 수행할 수 있도록 카운트를 Int에서 Double로 명시적으로 변환한다
다른 곳에 쓰는 제네릭 where 절에 대해 할 수 있는 것처럼
익스텐션의 일부인 제네릭 where 절에 여러 요구 사항을 포함할 수도 있다
목록의 각 요구 사항을 쉼표로 구분한다
Contextual Where Clauses
제네릭 타입의 컨텍스트에서 이미 작업 중인 경우, 제네릭 타입 제약 조건이 없는 선언의 일부로 제네릭 where 절을 작성할 수 있다
예를 들어 제네릭 타입의 서브스크립트 또는 제네릭 타입의 익스텐션에 있는 메서드에 제네릭 where 절을 작성할 수 있다
컨테이너 구조는 일반적이며, 아래 예제의 where 절은
컨테이너에서 이러한 새 메서드를 사용할 수 있도록 하기 위해 충족해야 하는 타입 제약 조건을 지정한다
extensionContainer{func average()->Doublewhere Item ==Int{varsum=0.0forindexin0..<count {
sum +=Double(self[index])}return sum / Double(count)}func endsWith(_ item:Item)->Boolwhere Item:Equatable{return count >=1 && self[count-1]== item
}}letnumbers=[1260,1200,98,37]print(numbers.average())
// Prints "648.75"
print(numbers.endsWith(37))
// Prints "true"
item이 정수일 때 컨테이너에 average() 메서드를 추가하고
item이 equatable일 때 endWith(_:) 메서드를 추가한다
두 함수 모두 컨테이너의 원래 선언에서 제네릭 Item 타입 매개변수에 형식 제약 조건을 추가하는 제네릭 where 절을 포함한다
컨텍스트 where 절을 사용하지 않고 이 코드를 작성하려면
각 제네릭 where 절에 대해 하나씩 두 개의 익스텐션을 작성한다
위의 예와 아래의 예시는 동작이 동일하다
extensionContainerwhere Item ==Int{func average()->Double{varsum=0.0forindexin0..<count {
sum +=Double(self[index])}return sum / Double(count)}}extensionContainerwhere Item:Equatable{func endsWith(_ item:Item)->Bool{return count >=1 && self[count-1]== item
}}
contextental where 절을 사용하는 위 예제의 버전에서
average()와 endsWith(_:)의 구현은 모두 동일한 익스텐션에 있다
왜냐하면 각 메서드의 제네릭 where 절은 해당 메서드를 사용할 수 있도록 하기 위해 충족해야 하는 요구 사항을 명시하기 때문이다
이러한 요구사항을 익스텐션의 제네릭으로 이동하면 동일하게 메서드를 사용할 수 있지만 요구사항당 하나의 확장이 필요하게 된다
Associated Types with a Generic Where Clause
Associated 타입에 제네릭 where 절을 포함할 수 있다
예를 들어, 표준 라이브러리에서 사용하는 Sequence 프로토콜처럼, iterator를 포함하는 Container 버전을 만들려고 한다
Iterator의 제네릭 where 절에서는 Iterator의 타입에 관계없이 컨테이너의 item과 동일한 item 타입의 요소를 통과해야 한다
makeIterator() 함수는 컨테이너의 Iterator에 대한 접근을 제공한다
다른 프로토콜에서 상속되는 프로토콜의 경우 프로토콜 선언에 제네릭 where 절을 포함하여
상속된 Associated 타입에 제약 조건을 추가한다
예를 들어, 다음 코드는 항목이 Comparable을 준수해야 하는 Comparable Container 프로토콜을 선언한다
서브 스크립트에도 제네릭을 사용할 수 있고 where절을 사용하여 제약조건을 줄 수도 있다
서브 스크립트 뒤에 있는 대괄호 안에 placeholder 타입 이름을 쓰고
서브 스크립트 본문의 열림 중괄호 바로 앞에 제네릭 where 절을 작성한다
extensionContainer{
subscript<Indices:Sequence>(indices:Indices)->[Item]where Indices.Iterator.Element ==Int{varresult=[Item]()forindexin indices {
result.append(self[index])}return result
}}
Container 프로토콜에 대한 익스텐션은 Sequence를 준수하는 Indices를 사용하고
각 인덱스의 item을 포함하는 배열을 반환하는 서브 스크립트를 추가한다
해당 제네릭 서브 스크립트는 다음과 같이 제한된다
<>로 묶인 매개변수 Indices는 Sequence 프로토콜을 준수하는 타입 이어야 한다
서브 스크립트는 매개변수로 Indices 타입의 인스턴스를 사용한다
매개변수 indices에 전달된 값은 Int 타입이다
이러한 제약 조건은 indices 매개 변수에 대해 전달된 값이 정수의 sequence라는 걸 의미한다
The text was updated successfully, but these errors were encountered:
Generic(제네릭) 코드를 사용하면 정의한 요구 사항에 따라 모든 타입에서 작동할 수 있는
유연하고 재사용 가능한 함수와 타입을 작성할 수 있다
이는 중복을 피하고 명확하고 추상적인 방식으로 코드를 작성할 수 있다
제네릭은 Swift의 가장 강력한 기능 중 하니이며, Swift 표준 라이브러리의 대부분은 제네릭 코드로 빌드된다
사실 지금까지 작성한 모든 글에서 제네릭을 사용하고 있었다
예를 들어 Swift의 배열, 딕셔너리 타입은 모두 제네릭 컬렉션이다
즉, 배열에 Int, String등 모든 타입을 저장할 수 있는 이유가 제네릭 타입이기 때문에 저장되는 타입에 제한이 없다
제네릭이 해결할 문제
다음 코드는 제네릭을 사용하지 않은 함수 swapTwoInts(_: _: )를 정의한 것이다
swapTwoInts(::) 함수는 In-Out 매개변수를 사용하여 원래 값인 b를 a로, 원래 값인 a를 b로 바꾼다
함수를 호출하여 두 Int 변수의 값을 바꿀 수 있다
swapTwoInts는 Int 값에만 적용할 수 있다는 한계가 있다
만약 String 값 또는 Double 값을 바꾸고 싶다면 새로 함수를 작성해야 한다
swapTwoInts(_: ), swapTwoStrings(_: ) 및 swapTwoDoubles(_: _: ) 함수의 본문이 동일하다는 것을 알 수 있다
유일한 차이점은 허용되는 값의 타입(Int, String 및 Double)이다
모든 타입의 두 값을 교환하는 단일 함수를 작성하는 것이 더 유용하고 훨씬 유연하다
제네릭 코드를 사용하면 이러한 함수를 작성할 수 있다
즉, 동일한 동작을 하는 코드를 다양한 타입에서 사용하기 위해선 제네릭 코드를 사용하면 된다
제네릭 함수(Generic Functions)
제네릭 함수는 모든 타입과 함께 사용할 수 있다
다음은 위에서 가져온 swapTwoInts(_: _: ) 함수의 제네릭을 활용한 swapTwoValues(_: _: ) 함수다
swapTwoValues(_: _: ) 함수의 본문은 swapTwoInts(_: _: ) 함수의 본문과 동일하다
그러나, swapTwoValues(_: )의 첫 번째 줄은 swapTwoInts(_: )와 약간 다르다
첫 번째 줄은 다음과 같이 비교된다
함수를 제네릭으로 만들면 타입 이름 대신(ex. Int, Double.. ) 를 사용한다
이는 placeholder 타입이라고 하며 개발자는 T가 무슨 타입인지 정하진 않았지만
a,b는 모두 같은 타입 T라는 것은 선언한 것으로
T 대신 사용할 실제 타입은 swapTwoValues(_: _: ) 함수를 호출할 때마다 결정된다
제네릭 함수와 기존 함수와의 차이점은 제네릭 함수는 정의할 때 라는 것을 적어준다
여기서 꺾은 괄호는 swapTwoValues(_: )에서 함수 이름과는 상관없이 정의 내 placeholder 타입의 이름이라는 걸 알리는 용도이며
T는 placeholder 타입이기 때문에 Swift는 T라는 실제 타입을 찾지 않는다
swapTwoValues(_: _: ) 함수는 이제 swapTwoInts와 동일한 방식으로 호출할 수 있다
두 값이 서로 동일한 타입이면 모든 타입의 두 값을 전달할 수 있다
swapTwoValues(_: _: )가 호출될 때마다 T에 사용할 타입은 함수에 전달된 값의 타입으로 추론된다
아래의 두 예시에서, T는 각각 Int와 String으로 추론된다
Type Parameters
위의 swapTwoValues(_: _: ) 예제에서 placeholder 타입 T는 타입 매개 변수의 예시이다
타입 매개 변수는 placeholder 타입을 지정하고 이름을 지정하며,
함수 이름 바로 뒤에 일치하는 한 쌍의 꺾은 괄호(예: ) 사이에 기록됩니다
즉, 여기서 지정한 타입 매개 변수의 이름은 T아다. 만약 <B>라고 함수 뒤에 썼다면 타입 매개 변수의 이름은 B다
타입 매개 변수를 지정한 후에는 함수 매개 변수의 타입(swapTwoValues(_: _: ) 함수의 a 및 b 매개 변수 등)을 정의하거나
함수의 반환 타입 또는 함수 본문 내의 타입 주석으로 정의할 수 있다
각각의 경우, 타입 파라미터는 함수가 호출될 때마다 실제 타입으로 대체된다
위의 swapTwoValues(_: _: ) 예제에서는 함수를 처음 호출할 때 T가 Int로 대체되고, 두 번째 호출할 때 String으로 대체된다
만약 타입 매개변수가 여러 개 필요하다면 <T, B>와 같이
여러 타입 매개 변수 이름을 꺾은 괄호 안에 쉼표로 구분하여 작성하여 둘 이상의 타입 매개 변수를 제공할 수 있다
Naming Type Parameters
대부분의 경우 타입 매개 변수는 설명이 포함된 이름을 가지고 있다
이는 타입 매개 변수와 해당 매개 변수가 사용되는 제네릭 타입 또는 함수 사이의 관계를 독자에게 알려준다
ex. Dictionary의 <Key, Value>과 같이 이름을 만든 것도 확인 가능
그러나, 이들 사이에 유의미한 관계가 없는 경우에는 처럼 단일 문자를 사용하는 것이 일반적이다
제네릭 타입(Generic Types)
제네릭 함수 외에도 Swift를 사용하면 자신만의 제네릭 타입을 정의할 수 있다
이러한 커스텀 클래스, 구조체 및 열거형은 배열 및 딕셔너리와 유사한 방식으로 모든 타입과 함께 사용할 수 있다
이번 섹션에서는 Stack이라는 일반 컬렉션 타입을 작성하는 방법을 보여준다
Stack은 Array와 비슷하지만 보다 제한된 LIFO(Last In First Out)의 규칙을 따르는 정렬된 값의 집합이다
따라서, 컬렉션 끝에서만 항목을 추가하고 제거할 수 있게 되며 값을 추가하는 것은 PUSH, 값을 제거하는 것은 POP으로 불린다
아래 그림은 스택의 PUSH와 POP을 나타낸 그림이다. 왼쪽부터 설명을 하면 다음과 같다
이러한 스택을 제네릭을 사용하지 않고 Int 타입만 가능하도록 만들어 보자.
해당 구조는 items이라는 배열 프로퍼티를 사용하여 값을 스택에 저장한다
Stack은 스택에서 값을 PUSH하고 POP하는 두 가지 메서드를 제공한다
이러한 메서드는 구조체의 items 배열을 수정(또는 변경)해야 하기 때문에 mutating으로 표시된다
그러나, 위에 표시된 IntStack 타입은 오로지 Int형에서만 사용할 수 있다
어떤 타입의 값이라도 스택을 관리할 수 있는 제네릭 스택 구조를 정의하는 것이 훨씬 더 유용하다
이를 모든 자료형이 사용할 수 있도록 제네릭을 함께 사용하면 다음과 같다
Stack의 제네릭 버전은 이전 버전과 동일하지만 실제 타입의 Int가 아닌 Element라는 타입 매개 변수를 사용한다
때문에 구조체 안에서 타입의 이름으로 Element를 사용하는 것을 볼 수 있다
타입 매개 변수는 구조체 이름 바로 뒤에 있는 한 쌍의 꺾은 괄호() 안에 기록된다
Element는 나중에 제공할 타입에 대한 placeholder 타입 이름을 정의한다
나중에 제공되는 타입인 Element는 구조체 정의 안에서 사용되며, 다음 세 위치에서 placeholder로 사용된다
Element 타입은 처음부터 타입의 종류가 정해진 것이 아니고 사용될 때 정해진다
제네릭 타입이기 때문에 Stack을 사용하여 배열, 딕셔너리와 유사한 방식으로 Stack을 만들 수 있다
스택에 저장할 타입을 꺾은 괄호 안에 입력하여 새 스택 인스턴스를 만든다
ex. 문자열의 새 스택을 만들려면 Stack()을 작성
위의 코드를 실행하면 그림과 같은 방식으로 실행된다
스택에 아래 값을 입력하면 상위 값인 "cuatro"가 제거되고 반환된다
fromTheTop에는 "cuatro"가 들어있게 되고 스택에는 3개의 값만 남는다
Extending a Generic Type
제네릭 타입에 익스텐션을 사용할 때 타입 매개변수는 제공하지 않아도 된다
하지만, 원본 제네릭 타입을 정의할 때 정의한 타입 매개변수 목록은 익스텐션 본문 안에서 사용할 수 있으며
타입 매개변수들의 이름도 그대로 사용할 수 있다
다음 예제에서는 제네릭 스택 타입을 익스텐션하여 topItem이라는 읽기 전용 계산 프로퍼티를 추가한다
해당 프로퍼티는 스택에서 최상위 item을 POP 없이 스택에서 반환한다
이때 반환 값의 타입은 아까 정의한 타입 매개변수인 Element 타입의 옵셔널한 값을 반환한다
스택이 비어있는 경우 topItem은 nil을 반환하고, 스택이 비어 있지 않으면 topItem은 items 배열의 마지막 item을 반환한다
위의 코드에서 볼 수 있듯이 제네릭 타입에 익스텐션을 사용할 땐 타입 매개변수 목록을 정의하지 않는다
하지만, 원래 코드에서 정의한 타입 매개변수 목록을 그대로 사용할 수 있다
이제 topItem 계산 프로퍼티를 스택 인스턴스에서 사용하여 상위 item을 제거하지 않고 액세스하고 쿼리할 수 있다.
제네릭 타입의 익스텐션에는 확장 타입의 인스턴스가 새로운 기능을 얻기 위해 충족해야 하는 요구 사항도 포함될 수 있다
이에 대한 내용은 Extensions with a Generic Where Clause 페이지에서 더 확인할 수 있다
Type Constraints
이 글에서 구현한 swapTwoValues,Stack은 모든 타입에서 사용될 수 있었다
하지만, 제네릭 함수와 타입에서 사용할 수 있는 타입에 제약 조건을 주는 것이 유용한 경우가 있다
타입 제약조건을 만족하기 위해선 타입 매개 변수가 특정 클래스를 상속하거나 특정 프로토콜을 준수해야 한다
예를 들어 Swift의 Dictionary는 Key로 사용할 수 있는 타입에 제약을 뒀다
Dictionary의 Key에 사용될 수 있는 타입은 반드시 hashable이어야 한다
즉, 고유하게 표현할 수 있는 id와 같은 방법을 제공해야 한다
Dictionary에는 특정 키에 대한 값이 이미 포함되어 있는지 확인할 수 있어야하고
이러한 제약조건이 없으면 Dictionary는 특정 키에 대한 값을 삽입할지 바꿀지 알 수 없으며
이미 Dictionary에 있는 특정 키에 대한 값을 찾을 수도 없다
이러한 제약조건은 Dictionary의 Key 타입에 대한 타입 제약조건에 의해 생기며
제약조건을 만족하기 위해선 Hashable 프로토콜을 만족하는 타입을 사용해야 한다
Swift의 기본적인 타입인 String, Int, Double 등은 Hashable 프로토콜을 준수하므로 Dictionary에서 사용할 수 있다
직접 제네릭 타입을 만들 때 고유한 제약 조건을 정의할 수 있으며, 이러한 제약조건은 제네릭 프로그래밍의 많은 기능을 제공할 수 있다
Hashable과 같은 추상적 개념은 구체적인 타입이 아닌 개념적 특성 측면에서 타입을 특성화한다
1) Type Constraint 문법
타입 매개 변수를 지정할 때 :으로 구분된 타입 매개 변수 이름 뒤에 단일 클래스나 프로토콜 제약 조건을 작성하여 제약조건을 만들 수 있다
예를 들어 위의 코드에는 두 개의 타입 매개 변수가 있다
T는 SomeClass 클래스의 서브 클래스여야 하고
U는 SomeProtocol 프로토콜을 준수하는 타입 이어야 한다
위와 같이 타입 매개 변수에 제약조건을 만들 수 있다
2) Type Constraint in Action
아래 정의한 findIndex(ofString:in:) 함수는 제네릭을 사용하지 않은 함수다
함수의 매개 변수는 String,[String] 타입을 가진다
findIndex(ofString:in:) 함수는 옵셔널 Int 값을 반환한다
이 값은 배열에서 처음 일치하는 문자열이 있으면 인덱스가 되고, 문자열을 찾을 수 없으면 nil이 된다
findIndex(ofString:in:) 함수를 사용하여 문자열 배열에서 문자열 값의 인덱스를 찾을 수 있다
배열에서 값의 인덱스를 찾는 원리가 문자열에만 유용한 것은 아니다
하지만, 위의 함수는 String 타입에만 사용할 수 있으므로 모든 타입에 사용할 수 있는 제네릭 함수로 바꿔보자
문자열에 대한 언급을 T 타입의 값으로 대체하여 기존의 함수와 동일한 기능을 쓸 수 있다
다음은 findIndex(of:in:)라는 제네릭 버전의 findIndex(String:in:)가 작성되는 방법이다
함수는 배열에서 옵셔널한 값이 아닌 옵셔널한 인덱스 번호를 반환하므로 함수의 반환 타입은 여전히 Int?이다
단, 해당 함수는 컴파일되지 않으므로 주의해야 한다
함수는 위에 작성한 것처럼 컴파일되지 않는데, 문제는 동일성 검사 "if value == valueToFind"에 있다
Swift의 모든 타입을 연산자(==)와 비교할 수 있는 것은 아니다
예를 들어, 복잡한 데이터 모델을 나타내는 고유한 클래스나 구조를 만든다면
해당 클래스나 구조에 대한 "동일"의 의미는 Swift가 추측할 수 있는 것이 아니게 된다
때문에 가능한 모든 타입 T에서 해당 코드가 작동한다는 것을 보장할 수 없으며, 코드를 컴파일하려고 할 때 오류가 발생한다
그러나, 완전히 불가능한 건 아니다
Swift 표준 라이브러리는 Equatable이라고 불리는 프로토콜을 정의하는데,
해당 프로토콜을 따르는 타입의 값을 비교하기 위한 비교 연산자인 (==)와 (!==)을 구현할 수 있도록 하며
Swift의 모든 표준 타입은 자동으로 Equatable 프로토콜을 지원한다
equatable인 모든 타입은 equal to 연산자를 사용할 수 있으므로
findIndex(of:in:) 함수와 함께 안전하게 사용할 수 있다
해당 사실을 표현하기 위해 함수를 정의할 때 타입 매개 변수 정의의 일부로 Equatable의 타입 제약 조건을 작성한다
findIndex(of:in:)의 단일 타입 매개 변수는 T: Equatable로 작성되며, 이는 "Equatable 프로토콜에 부합하는 모든 타입"을 의미한다
이제 findIndex(of:in:) 함수가 성공적으로 컴파일되고, Double 또는 String과 같이 동등하게 사용할 수 있다
Associated Types
프로토콜을 정의할 때 정의의 일부로 하나 이상의 associated 타입을 선언하는 경우가 유용할 수 있다
associated 타입은 프로토콜의 일부로 사용되는 타입에 placeHolder 타입 이름을 제공한다
프로토콜에서 이러한 타입을 정의할 땐 associatedtype 키워드를 함께 사용해줘야 한다
1) Associated Types in Action
다음은 Item이라는 Associated Type을 선언하는 Container 프로토콜의 예시이다
Container 프로토콜은 제공해야 하는 세 가지 필수 기능을 정의한다
프로토콜은 컨테이너의 item을 저장하는 방법이나 허용되는 타입을 지정하지 않는다
프로토콜은 컨테이너로 간주되기 위해 모든 타입이 제공해야 하는 3가지의 기능만 지정한다
적합한 타입은 세 가지 요건을 충족하는 한 추가 기능을 제공할 수 있다
Container 프로토콜을 준수하는 모든 타입은 저장되는 값의 타입을 지정할 수 있어야 한다
특히 올바른 타입의 item만 컨테이너에 추가되도록 해야 하며, 서브 스크립트가 반환하는 item의 타입에 대해 명확해야 한다
이러한 요구 사항을 정의하기 위해 Container 프로토콜은 특정 컨테이너에 대해
해당 타입이 무엇인지 알지 못한 채 컨테이너가 보유할 요소의 타입을 참조하는 방법이 필요하다
Container 프로토콜은 append(_:) 메서드에 전달된 값이 컨테이너의 Item 타입과 동일해야 하며
컨테이너의 서브 스크립트가 반환하는 값이 컨테이너의 Item 타입과 동일하도록 지정해야 한다
이를 위해 Container 프로토콜은 Item이라는 관련 타입을 선언하고 associatedtype Item으로 작성한다
프로토콜은 Item이 무엇인지는 정의하지 않는다
해당 정보는 적합한 타입이 차후에 제공할 수 있도록 남겨지고, 그럼에도 불구하고 Item alias는 컨테이너의 Item 타입을 참조하고
append(_ :) 메서드 및 서브 스크립트와 함께 사용할 타입을 정의하여, 컨테이너에서 예상 동작이 적용되도록 하는 방법을 제공한다
다음은 Container 프로토콜을 준수하는 제네릭을 사용하지 않은 IntStack 구조체이다
위의 IntStack 코드는 Container 프로토콜을 준수하는 제네릭을 사용하지 않은 구조체이다
IntStack 구조체는 프로토콜에서 요구한 Item 타입의 실제 타입으로 Int를 사용하게 된다
IntStack 타입의 기존 기능의 일부를 랩핑하여 이러한 요구 사항을 충족한다
IntStack은 Container 프로토콜의 구현을 위해, 사용할 적절한 Item이 Int의 한 타입이라고 명시한다
typealias Item = Int의 정의는 Container 프로토콜 구현을 위해 Item의 추상 타입을 Int의 구체적인 타입으로 바꾼다
Swift의 타입 추론 덕분에 IntStack 정의의 일부로 구체적인 Int의 Item를 선언할 필요가 없다
Swift는 append(_:) 메서드의 item 매개 변수 타입과 서브 스크립트의 반환 타입을 보는 것만으로 사용할 적절한 item을 추론하며,
실제로 위의 코드에서 Item = Int 라는 부분을 삭제해도 Item에 사용해야 하는 타입이 명확하기 때문에 모든 것이 여전히 작동한다
하지만, 이렇게 구현하면 다른 타입에 대해서는 사용할 수 없기 때문에 제네릭을 사용하여 다시 구현해보자
위와 같이 제네릭을 사용한 Stack 구조체를 정의할 수 있다
여기서는 타입 매개 변수로 Element 라는 이름을 사용했다
Element가 append(_:) 메서드의 item 매개 변수의 타입과 서브 스크립트의 반환 타입으로 사용된다
따라서, Swift는 Element가 해당 특정 컨테이너의 Item으로 사용하기에 적합한 타입이라고 추론할 수 있다
이젠 Container 프로토콜을 준수하는 Stack 구조체에 다양한 타입을 사용할 수 있다
3) Extending an Existing Type to Specify an Associated Type
익스텐션을 사용하여 기존 타입에 프로토콜을 준수하도록 추가할 수 있다
여기에는 Associated 타입을 가진 프로토콜도 포함된다
아래와 같이 Array 타입에 Container 프로토콜을 추가로 채택할 수 있다
Array 타입에는 이미 append(_ :) 메서드, count 프로퍼티, Int인덱스를 사용하는 서브 스크립트가 존재하기 때문에
Container의 모든 요구 사항을 준수한다
배열의 기존 append(_ :) 메서드와 서브 스크립트를 사용하면
위의 제네릭 스택 타입 예시와 마찬가지로 Swift가 항목에 사용할 적절한 타입을 추론할 수 있다
익스텐션을 정의한 후에는 모든 배열을 컨테이너로 사용할 수 있다
4) Adding Constrints to an Associated Type
프로토콜에 정의된 Associated 타입에도 타입 제약조건을 추가할 수 있다
위와 같이 Item에 Equatable 프로토콜을 준수해야 한다는 제약조건을 추가할 수 있다
5) Using a Protocol in Its Associated Type's Constraints
프로토콜은 자체 요구 사항의 일부로 나타낼 수 있다.
예를 들어, 다음은 suffix(_:) 메서드 요구 사항을 추가하여 Container 프로토콜을 세분화하는 프로토콜이다
suffix(_:) 메서드는 컨테이너 끝에서 지정된 수의 요소를 Suffix 타입의 인스턴스 내부에 저장하여 반환한다
위의 코드는 Container 프로토콜에 요구사항을 추가한 SuffixableContainer 프로토콜을 정의한 예시다
SuffixableContainer 프로토콜 Suffix 라는 associated 타입과 suffix(_ :) 메서드를 추가로 요구한다
Suffix 타입은 SuffixableContainer 프로토콜을 준수해야 하고, 해당 Item 타입은 Container 프로토콜의 Item 타입과 동일해야 한다
Item에 대한 제약 조건은 제네릭 where 구문에 나타난다
Associated Types with a Generic Where Clause 페이지에서 관련 정보를 더 얻을 수 있다
다음은 위의 Generic Types의 Stack 타입에 SuffixableContainer 프로토콜을 추가로 채택하여 익스텐션한다
해당 코드에서 Suffix의 associated 타입도 Stack이므로 suffix(_ :) 메서드는 다른 Stack 타입을 반환한다
또한, SuffixableContainer를 준수하는 타입은 자신과는 다른 Suffix 타입을 가질 수 있다
즉, suffix 작업은 다른 타입을 반환할 수도 있다
아래의 코드는 제네릭을 사용하지 않은 IntStack 구조체에 익스텐션을 사용하여
SuffixableContainer 프로토콜을 추가로 채택하는 코드다
Suffix의 타입으로 IntStack대신 Stack를 사용한 것을 볼 수 있다
Generic Where Clauses
타입 제약 조건을 사용하면 제네릭 함수, 서브 스크립트, 타입에서
associated 타입 매개 변수에 대한 요구사항을 정의할 수 있다
Associated 타입에 대해 요구사항을 정의하는 것도 유용할 수 있다
이러한 정의는 제네릭 where 절을 정의하여 할 수있다
제네릭 where절을 사용하면 associated 타입이 특정 프로토콜을 준수해야 하거나
특정 타입 매개변수와 동일해야 한다고 요구할 수 있다
제네릭 where 절은 where키워드로 시작하고 그 뒤에 associated 타입에 대한 제약조건, 동등관계를 써주면 된다
아래 예제에서는 두 Container 인스턴스에 동일한 항목이 동일한 순서로 포함되어 있는지 확인하는
allItemsMatch라는 일반 함수를 정의한다
해당 함수는 모든 항목이 일치하면 true를 반환하고 일치하지 않으면 false 값을 반환한다
검사할 두 컨테이너가 동일한 타입의 컨테이너일 필요는 없지만, 동일한 타입의 items를 포함해야 한다
해당 요구사항은 타입 제약조건과 일반 where 절의 조합을 통해 표현된다
이 함수는 someContainer와 anotherContainer라는 두 개의 인수를 사용한다
컨테이너 인수는 C1 타입 다른 컨테이너 인수는 C2 타입이다
C1과 C2는 모두 함수가 호출될 때 결정되는 두 컨테이너 타입에 대한 타입 매개변수다
함수의 두 가지 타입 매개변수에는 다음과 같은 요구사항이 있다
첫 번째 요건과 두 번째 요건은 함수의 타입 매개변수 목록에 정의되며, 세 번째 요건과 네 번째 요건은 함수의 제네릭 where 절에 정의된다
이러한 요구사항은 다음을 의미한다
세 번째와 네 번째 요구 사항은 otherContainer의 items이 someContainer의 items와 정확히 동일한 타입이기 때문에
부등 연산자(!=)를 통해 확인할 수 있음을 의미한다
이러한 요구 사항을 통해 allItemsMatch(_:) 함수가 두 컨테이너 타입이 다른 경우에도 비교할 수 있다
allItemsMatch(::) 함수은 두 컨테이너에 동일한 수의 items이 포함되어 있는지 확인하는 것으로 시작한다
서로 다른 수의 items을 포함하는 경우 일치시킬 방법이 없으며 함수는 false를 반환한다
해당 검사를 수행한 후, 함수는 for-in 루프와 반 열림 범위 연산자(..<)가 있는 일부 컨테이너의 모든 item에 걸쳐 순회한다
함수는 각 item에 대해 someContainer의 item이 otherContainer의 해당 item과 동일하지 않은지 확인한다
두 item이 동일하지 않으면 두 컨테이너가 일치하지 않고 함수가 false를 반환한다
일치하지 않는 item을 찾지 않고 루프가 완료되면 두 컨테이너가 일치하고 함수가 true를 반환한다
allItemsMatch(_:_:) 함수가 작동하는 방식은 다음과 같다
위의 예제는 Stack 인스턴스를 생성하여 String 값을 저장하고 세 개의 문자열을 스택에 푸시한다
예제에서는 또한 스택과 동일한 세 개의 문자열을 포함하는 배열 리터럴로 초기화된 배열 인스턴스를 만든다
스택과 배열이 서로 다른 타입이지만 Container 프로토콜을 준수하며 둘 다 동일한 타입의 값을 포함한다
따라서, 이 두 컨테이너를 인수로 하여 allItemsMatch(_:) 함수를 호출할 수 있다
위의 예에서 allItemsMatch(_:_:) 함수는 두 컨테이너의 모든 항목이 일치한다고 올바르게 보고한다
Extensions with a Generic Where Clause
제네릭 where절을 익스텐션에서도 사용할 수 있다
아래 코드는 제네릭을 사용한 Stack 구조체에 익스텐션을 사용할 때 where절도 사용한 코드이다
익스텐션으로 추가한 isTop(_ :) 메서드는 매개변수로 받은 값이 현재 인스턴스의 제일 위에 존재하는 값인지를 확인해주는 메서드이다
여기서 타입 매개변수로 사용할 Element는 Equatable 프로토콜을 준수해야 한다. 만약 그렇지 않다면 컴파일 에러를 발생한다
isTop(:) 메서드는 먼저 스택이 비어 있지 않은지 확인한 다음 지정된 item을 스택의 맨 위 item과 비교한다
제네릭 where 절 없이 해당 작업을 수행하려고 하면 다음과 같은 문제가 발생한다
isTop(:)의 구현에서는 == 연산자를 사용하지만
Stack의 정의에서는 해당 item이 equatable할 필요가 없으므로
== 연산자를 사용하면 컴파일 타임 오류가 발생한다
제네릭 where 절을 사용하면 익스텐션에 새 요구 사항을 추가할 수 있으므로
스택의 item이 equatable한 경우에만 익스텐션에서 isTop(_:) 메서드를 추가할 수 있다
요소가 equatable하지 않은 스택에서 isTop(_:) 메서드를 호출하려고 하면 컴파일 시간 오류가 발생한다
프로토콜 익스텐션과 함께 제네릭 where 절을 사용할 수 있다
아래 예제는 이전 예제의 Container 프로토콜을 익스텐션하여 startsWith(_:) 메서드를 추가한다
startsWith(:) 메서드는 먼저 컨테이너에 하나 이상의 item이 있는지 확인한 다음
컨테이너의 첫 번째 item이 지정된 item과 일치하는지 확인한다
새로운 startsWith(:) 메서드는 컨테이너의 item이 동일하다면
위에서 사용된 스택과 배열을 포함하여 Container 프로토콜에 부합하는 모든 타입에 사용할 수 있다
위의 예시에서 generic where 절은 Item이 프로토콜을 준수하도록 요구하지만,
Item이 특정 타입이어야 하는 절에 generic where를 작성할 수도 있다
예시에서는 Item 타입이 Double인 컨테이너에 average() 메서드를 추가한다
컨테이너의 item을 순회하여 합산하고, 컨테이너의 개수로 나누어 평균을 계산한다
부동 소수점 나눗셈을 수행할 수 있도록 카운트를 Int에서 Double로 명시적으로 변환한다
다른 곳에 쓰는 제네릭 where 절에 대해 할 수 있는 것처럼
익스텐션의 일부인 제네릭 where 절에 여러 요구 사항을 포함할 수도 있다
목록의 각 요구 사항을 쉼표로 구분한다
Contextual Where Clauses
제네릭 타입의 컨텍스트에서 이미 작업 중인 경우, 제네릭 타입 제약 조건이 없는 선언의 일부로 제네릭 where 절을 작성할 수 있다
예를 들어 제네릭 타입의 서브스크립트 또는 제네릭 타입의 익스텐션에 있는 메서드에 제네릭 where 절을 작성할 수 있다
컨테이너 구조는 일반적이며, 아래 예제의 where 절은
컨테이너에서 이러한 새 메서드를 사용할 수 있도록 하기 위해 충족해야 하는 타입 제약 조건을 지정한다
item이 정수일 때 컨테이너에 average() 메서드를 추가하고
item이 equatable일 때 endWith(_:) 메서드를 추가한다
두 함수 모두 컨테이너의 원래 선언에서 제네릭 Item 타입 매개변수에 형식 제약 조건을 추가하는 제네릭 where 절을 포함한다
컨텍스트 where 절을 사용하지 않고 이 코드를 작성하려면
각 제네릭 where 절에 대해 하나씩 두 개의 익스텐션을 작성한다
위의 예와 아래의 예시는 동작이 동일하다
contextental where 절을 사용하는 위 예제의 버전에서
average()와 endsWith(_:)의 구현은 모두 동일한 익스텐션에 있다
왜냐하면 각 메서드의 제네릭 where 절은 해당 메서드를 사용할 수 있도록 하기 위해 충족해야 하는 요구 사항을 명시하기 때문이다
이러한 요구사항을 익스텐션의 제네릭으로 이동하면 동일하게 메서드를 사용할 수 있지만 요구사항당 하나의 확장이 필요하게 된다
Associated Types with a Generic Where Clause
Associated 타입에 제네릭 where 절을 포함할 수 있다
예를 들어, 표준 라이브러리에서 사용하는 Sequence 프로토콜처럼, iterator를 포함하는 Container 버전을 만들려고 한다
Iterator의 제네릭 where 절에서는 Iterator의 타입에 관계없이 컨테이너의 item과 동일한 item 타입의 요소를 통과해야 한다
makeIterator() 함수는 컨테이너의 Iterator에 대한 접근을 제공한다
다른 프로토콜에서 상속되는 프로토콜의 경우 프로토콜 선언에 제네릭 where 절을 포함하여
상속된 Associated 타입에 제약 조건을 추가한다
예를 들어, 다음 코드는 항목이 Comparable을 준수해야 하는 Comparable Container 프로토콜을 선언한다
Generic Subscripts
서브 스크립트에도 제네릭을 사용할 수 있고 where절을 사용하여 제약조건을 줄 수도 있다
서브 스크립트 뒤에 있는 대괄호 안에 placeholder 타입 이름을 쓰고
서브 스크립트 본문의 열림 중괄호 바로 앞에 제네릭 where 절을 작성한다
Container 프로토콜에 대한 익스텐션은 Sequence를 준수하는 Indices를 사용하고
각 인덱스의 item을 포함하는 배열을 반환하는 서브 스크립트를 추가한다
해당 제네릭 서브 스크립트는 다음과 같이 제한된다
이러한 제약 조건은 indices 매개 변수에 대해 전달된 값이 정수의 sequence라는 걸 의미한다
The text was updated successfully, but these errors were encountered: