From fb02e6ac44dc6176070746202220831f699e8d25 Mon Sep 17 00:00:00 2001 From: Chaewan Park Date: Sat, 23 Jan 2021 11:33:28 +0900 Subject: [PATCH 1/3] =?UTF-8?q?item=2031=20=EC=B4=88=EC=95=88=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item31.md" | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 "5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" diff --git "a/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" new file mode 100644 index 0000000..15adf0c --- /dev/null +++ "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" @@ -0,0 +1,127 @@ +# Item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 + +### 한정적 와일드카드 사용 + +```java +public class Stack { + // ... + public void pushAll(Iterable src) { + for (E e : src) + push(e); + } +} +``` + +이렇게 작성할 경우, 매개변수화 타입은 불공변이기 때문에 `Stack`와 같이 선언할 경우 `Number`의 하위 타입인 `Integer`도 입력할 수 없다. + +```java +public void pushAll(Iterable src) { + // ... +} +``` + +위와 같이 와일드카드 연산자를 사용하여 Iterable 인터페이스를 구현한 E의 하위 타입도 입력할 수 있게 만들 수 있다. + +### 스위프트 + +스위프트는 와일드카드 연산자가 없는데다가, 연관 타입을 가진 프로토콜은 Sequence 형태로도 쓸 수 없음 + +```swift +public protocol Sequence { + associatedtype Element ... + // ... +} + +func f(sequence: Sequence) {} // 불가능 +func g() -> S where S.Element == Int { ... } // 복잡 +``` + +아래 형태는 복잡하며, 내부 구현인 Element 등을 외부로 노출하고 있다. + +일반화하여 사용할 수 있도록 하려면 Type Erasing을 사용해야 한다. + +### Type Erasure + +조금 더 일반적인 타입을 만들기 위해 특정 타입을 지우는 것을 말함. + +내부 구현 타입을 래핑하여 외부로부터 숨긴다. + +구체적인 타입이 코드 베이스에 퍼지는 것을 막는 효과 + +코드는 draft... + +```swift +// Sequence +class MAnySequence: Sequence { + class Iterator: IteratorProtocol { + func next() -> Element? { + fatalError("Must override next()") + } + } + func makeIterator() -> Iterator { // type-erased public API + fatalError("Must override makeIterator()") + } + static func make(_ seq: S) -> MAnySequence where S.Element == Element { + return MAnySequenceImpl(seq) + } +} + +private class MAnySequenceImpl: MAnySequence { + class IteratorImpl: Iterator { + var wrapped: S.Iterator + + init(_ wrapped: S.Iterator) { + self.wrapped = wrapped + } + override func next() -> S.Element? { + return wrapped.next() + } + } + + var sequence: S + + init(_ sequence: S) { + self.sequence = sequence + } + override func makeIterator() -> IteratorImpl { + return IteratorImpl(sequence.makeIterator()) + } +} + +MAnySequence.make([1, 2, 3, 4, 5]).forEach { print($0) } +MAnySequence.make([1 ..< 4]).forEach { print($0) } +``` + +연관 타입을 가진 프로토콜을 구체 타입으로 사용할 수 있도록 만들어준다. + +스위프트의 AnyCollection, AnyHashable 등의 erased type을 제공하고 있다. AnySequence 또한 Sequence를 래핑하여 타입을 모른 채로 iterate를 가능하도록 해 준다. + +### Type Erasure가 실제 사용되는 곳 + +```swift +public protocol ObserverType { + associatedtype Element + // ... +} + +// A type-erased ObserverType. +public struct AnyObserver : ObserverType { + // ... + public init(_ observer: Observer) where Observer.Element == Element { + self.observer = observer.on + } +} +``` + +```swift +private func f1(_ arg: ObserverType) {} // Error +private func f2(_ arg: AnyObserver) {} // Legal +``` + +### 결론 + +자바에서 불공변인 것 때문에 하위 타입을 넣을 수 없는 문제점이 있으니, 한정적 와일드카드를 사용하여 하위 타입도 입력을 가능하게 만든다. + +스위프트 제네릭은 아직 제약이 많아서 와일드카드를 사용하지 못할 뿐더러 제네릭 프로토콜의 경우 내부 구현 타입까지 명시해야 함. 일반적인 타입으로 사용하도록 만들기 위한 타입 이레이징 기법이 존재한다. + +하지만 스위프트의 제네릭 또한 자바처럼 불공변이며, 와일드카드가 없기 때문에 하위 타입을 넣을 수 없다. From a9270be990348e3ebc6b88185e2f98bad0000edf Mon Sep 17 00:00:00 2001 From: Chaewan Park Date: Sat, 6 Mar 2021 09:11:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?item=2031=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item31.md" | 138 ++++++++---------- 1 file changed, 62 insertions(+), 76 deletions(-) diff --git "a/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" index 15adf0c..0707482 100644 --- "a/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" +++ "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" @@ -1,6 +1,8 @@ # Item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라 -### 한정적 와일드카드 사용 +자바에서 매개변수화 타입은 불공변입니다. 이 때문에 제네릭을 사용한 API을 오직 매개변수화된 해당 타입 하나로만 사용할 수 있어서 API 유연성이 다소 떨어집니다. 이 때 한정적 와일드카드를 사용하면 하위 타입 또는 상위 타입도 입력할 수 있어 더 유연한 API를 만들 수 있습니다. 자바에서의 한정적 와일드카드에 대해 요약하고, Objective-C와 스위프트에서 이와 관련된 내용에 대해 정리해보겠습니다. + +### 자바의 한정적 와일드카드 사용하기 ```java public class Stack { @@ -12,7 +14,7 @@ public class Stack { } ``` -이렇게 작성할 경우, 매개변수화 타입은 불공변이기 때문에 `Stack`와 같이 선언할 경우 `Number`의 하위 타입인 `Integer`도 입력할 수 없다. +위와 같이 pushAll 메서드를 작성할 경우, 매개변수화 타입은 불공변이기 때문에 스택을 `Stack`로 선언한 후에 `Number`의 하위 타입인 `Integer` 타입의 요소를 push하려 한다면 에러가 발생합니다. ```java public void pushAll(Iterable src) { @@ -20,108 +22,92 @@ public void pushAll(Iterable src) { } ``` -위와 같이 와일드카드 연산자를 사용하여 Iterable 인터페이스를 구현한 E의 하위 타입도 입력할 수 있게 만들 수 있다. - -### 스위프트 +이렇게 한정적 와일드카드 타입을 사용하여 Iterable 인터페이스를 구현한 E의 하위 타입도 입력할 수 있도록 만들 수 있습니다. -스위프트는 와일드카드 연산자가 없는데다가, 연관 타입을 가진 프로토콜은 Sequence 형태로도 쓸 수 없음 +만약 pushAll과 반대인 popAll을 구현한다면, 스택 요소의 타입보다 상위 타입의 매개변수로 받아야 하므로 다음과 같이 super를 사용해야 합니다. -```swift -public protocol Sequence { - associatedtype Element ... - // ... +```java +public void popAll(Collection dst) { + while (!isEmpty()) + dst.add(pop()); } - -func f(sequence: Sequence) {} // 불가능 -func g() -> S where S.Element == Int { ... } // 복잡 ``` -아래 형태는 복잡하며, 내부 구현인 Element 등을 외부로 노출하고 있다. +### Objective-C에서의 `__covariant`와 `__contravariant` -일반화하여 사용할 수 있도록 하려면 Type Erasing을 사용해야 한다. +Objcective-C에는 `__covariant`와 `__contravariant`라는 키워드가 있습니다. 제네릭 파라미터 앞에 `__covariant`를 붙이는 것은 서브타입들을 받아들일 수 있음을 의미하며, `__contravariant`를 붙이는 것은 슈퍼타입들을 받아들일 수 있음을 의미합니다. -### Type Erasure +```objectivec +@interface Queue<__covariant ObjectType> : NSObject -조금 더 일반적인 타입을 만들기 위해 특정 타입을 지우는 것을 말함. +- (void)enqueue:(ObjectType)value; +- (ObjectType)dequeue; -내부 구현 타입을 래핑하여 외부로부터 숨긴다. +@end +``` -구체적인 타입이 코드 베이스에 퍼지는 것을 막는 효과 +### 스위프트 제네릭에서 하위 타입 받기 -코드는 draft... +스위프트 제네릭 또한 자바처럼 불공변입니다. ```swift -// Sequence -class MAnySequence: Sequence { - class Iterator: IteratorProtocol { - func next() -> Element? { - fatalError("Must override next()") - } - } - func makeIterator() -> Iterator { // type-erased public API - fatalError("Must override makeIterator()") - } - static func make(_ seq: S) -> MAnySequence where S.Element == Element { - return MAnySequenceImpl(seq) - } -} +class Garage { ... } -private class MAnySequenceImpl: MAnySequence { - class IteratorImpl: Iterator { - var wrapped: S.Iterator - - init(_ wrapped: S.Iterator) { - self.wrapped = wrapped - } - override func next() -> S.Element? { - return wrapped.next() - } - } - - var sequence: S - - init(_ sequence: S) { - self.sequence = sequence - } - override func makeIterator() -> IteratorImpl { - return IteratorImpl(sequence.makeIterator()) - } -} +func put(in garage: Garage) { ... } -MAnySequence.make([1, 2, 3, 4, 5]).forEach { print($0) } -MAnySequence.make([1 ..< 4]).forEach { print($0) } +put(in: Garage()) // 가능 +put(in: Garage()) // 에러 ``` -연관 타입을 가진 프로토콜을 구체 타입으로 사용할 수 있도록 만들어준다. +마지막 줄에서 Garage 타입을 받는 매개변수에 Garage를 입력하고 있어서 다음과 같은 에러가 발생합니다. -스위프트의 AnyCollection, AnyHashable 등의 erased type을 제공하고 있다. AnySequence 또한 Sequence를 래핑하여 타입을 모른 채로 iterate를 가능하도록 해 준다. +``` +Cannot convert value of type 'Garage' to expected argument type 'Garage' +``` -### Type Erasure가 실제 사용되는 곳 +하지만 스위프트에는 Objective-C처럼 서브타입 또는 슈퍼타입을 받아들일 수 있음을 나타내는 키워드는 없습니다. 스위프트 제네릭에서 서브 타입만을 입력할 수 있게 만드려면 다음과 같이 제네릭 type constraint를 이용할 수 있습니다. ```swift -public protocol ObserverType { - associatedtype Element - // ... -} +func put(in garage: Garage) { ... } -// A type-erased ObserverType. -public struct AnyObserver : ObserverType { - // ... - public init(_ observer: Observer) where Observer.Element == Element { - self.observer = observer.on - } -} +put(in: Garage()) +put(in: Garage()) // 가능 ``` +Type constraint를 이용해 Car의 서브클래스들만 입력할 수 있도록 제한하긴 했지만, 이는 자바의 한정적 와일드카드 타입과는 다른 기능이어서 완전히 동일한 역할을 수행해 주지는 않습니다. 또한 제네릭 type constraint를 이용해 슈퍼 타입만 받는 방법은 찾을 수 없었습니다. + +### 스위프트에서 공변인 경우 + +스위프트에서, 커스텀 타입의 제네릭은 불공변이지만 배열은 공변입니다. + ```swift -private func f1(_ arg: ObserverType) {} // Error -private func f2(_ arg: AnyObserver) {} // Legal +class Car { ... } +class PoliceCar: Car { ... } + +func drive(_ cars: Array) { ... } + +drive(Array()) +drive(Array()) // 가능 +``` + +그 이유를 추측하기 위해 NSArray의 Objective-C 인터페이스를 보면 다음과 같습니다. + +```objectivec +@interface NSArray<__covariant ObjectType> : NSObject ``` +제네릭 파라미터가 `__covariant`로 선언되어 있어 서브 타입들도 배열에 입력할 수 있습니다. 스위프트에서의 배열은 NSArray와 bridging되어 있어서 호환성을 위해 공변이 아닐까 추측됩니다. + ### 결론 -자바에서 불공변인 것 때문에 하위 타입을 넣을 수 없는 문제점이 있으니, 한정적 와일드카드를 사용하여 하위 타입도 입력을 가능하게 만든다. +자바에서 매개변수화 타입이 불공변인 것 때문에 제네릭을 이용한 API를 설계할 때 문제점이 있으니, 한정적 와일드카드 타입을 사용하여 API 유연성을 높이는 편이 좋습니다. + +Objective-C에서는 비슷한 역할을 하는 `__covariant`와 `__contravariant` 키워드가 있지만, 스위프트에는 이러한 기능이 없고, 제네릭 type constraint를 이용해 하위 타입만 받을 수 있도록 제약할 수 있긴 하지만, 자바의 한정적 와일드카드 타입과는 그 역할이 다릅니다. + +추가적으로, 스위프트에서 배열이 공변인 점에 대해 기술하고 그 이유를 추측해 보았습니다. + +### References -스위프트 제네릭은 아직 제약이 많아서 와일드카드를 사용하지 못할 뿐더러 제네릭 프로토콜의 경우 내부 구현 타입까지 명시해야 함. 일반적인 타입으로 사용하도록 만들기 위한 타입 이레이징 기법이 존재한다. +- [NSArray](https://developer.apple.com/documentation/foundation/nsarray?language=objc) +- [Covariance and Contravariance](https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html) -하지만 스위프트의 제네릭 또한 자바처럼 불공변이며, 와일드카드가 없기 때문에 하위 타입을 넣을 수 없다. From 9ea1fafb089abee2065bba0c1093042d1a841a0c Mon Sep 17 00:00:00 2001 From: Chaewan Park Date: Sat, 6 Mar 2021 09:26:39 +0900 Subject: [PATCH 3/3] =?UTF-8?q?item=2031=20=EC=86=8C=EC=A3=BC=EC=A0=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=20=EA=B3=B5=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../item31.md" | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git "a/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" index 0707482..d3e9784 100644 --- "a/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" +++ "b/5\354\236\245_\354\240\234\353\204\244\353\246\255/item31.md" @@ -2,6 +2,8 @@ 자바에서 매개변수화 타입은 불공변입니다. 이 때문에 제네릭을 사용한 API을 오직 매개변수화된 해당 타입 하나로만 사용할 수 있어서 API 유연성이 다소 떨어집니다. 이 때 한정적 와일드카드를 사용하면 하위 타입 또는 상위 타입도 입력할 수 있어 더 유연한 API를 만들 수 있습니다. 자바에서의 한정적 와일드카드에 대해 요약하고, Objective-C와 스위프트에서 이와 관련된 내용에 대해 정리해보겠습니다. +
+ ### 자바의 한정적 와일드카드 사용하기 ```java @@ -33,6 +35,8 @@ public void popAll(Collection dst) { } ``` +
+ ### Objective-C에서의 `__covariant`와 `__contravariant` Objcective-C에는 `__covariant`와 `__contravariant`라는 키워드가 있습니다. 제네릭 파라미터 앞에 `__covariant`를 붙이는 것은 서브타입들을 받아들일 수 있음을 의미하며, `__contravariant`를 붙이는 것은 슈퍼타입들을 받아들일 수 있음을 의미합니다. @@ -46,6 +50,8 @@ Objcective-C에는 `__covariant`와 `__contravariant`라는 키워드가 있습 @end ``` +
+ ### 스위프트 제네릭에서 하위 타입 받기 스위프트 제네릭 또한 자바처럼 불공변입니다. @@ -76,6 +82,8 @@ put(in: Garage()) // 가능 Type constraint를 이용해 Car의 서브클래스들만 입력할 수 있도록 제한하긴 했지만, 이는 자바의 한정적 와일드카드 타입과는 다른 기능이어서 완전히 동일한 역할을 수행해 주지는 않습니다. 또한 제네릭 type constraint를 이용해 슈퍼 타입만 받는 방법은 찾을 수 없었습니다. +
+ ### 스위프트에서 공변인 경우 스위프트에서, 커스텀 타입의 제네릭은 불공변이지만 배열은 공변입니다. @@ -98,6 +106,8 @@ drive(Array()) // 가능 제네릭 파라미터가 `__covariant`로 선언되어 있어 서브 타입들도 배열에 입력할 수 있습니다. 스위프트에서의 배열은 NSArray와 bridging되어 있어서 호환성을 위해 공변이 아닐까 추측됩니다. +
+ ### 결론 자바에서 매개변수화 타입이 불공변인 것 때문에 제네릭을 이용한 API를 설계할 때 문제점이 있으니, 한정적 와일드카드 타입을 사용하여 API 유연성을 높이는 편이 좋습니다. @@ -106,6 +116,8 @@ Objective-C에서는 비슷한 역할을 하는 `__covariant`와 `__contravarian 추가적으로, 스위프트에서 배열이 공변인 점에 대해 기술하고 그 이유를 추측해 보았습니다. +
+ ### References - [NSArray](https://developer.apple.com/documentation/foundation/nsarray?language=objc)