모든 문제는 또다른 프로토콜을 추가하여 해결할 수 있다.
옵셔널은 멋지다. 이제까지 나는 Objective-C의 "messages to nil return nil" 버그를 너무 많이 봐왔었고 다시 그때로 돌아가고 싶지도 않다.
그러나 당신은 옵셔널이나 특정 타입의 옵셔널이 필요할 때가 종종 있다. 아래에는 내가 즐겨쓰는 그 경우들이다.
isNilOrEmpty
가끔씩 nil과 isEmpty==true의 차이를 신경쓰지 않아도 될 때가 있다. 먼저 _CollectionOrStringish 프로토콜을 만든다. 이 프로토콜은 비어있고, 이 타입이 isEmpty 프로퍼티를 가진다는 것을 표시하여 사용한다.
protocol _CollectionOrStringish {
var isEmpty: Bool { get }
}
extension String: _CollectionOrStringish { }
extension Array: _CollectionOrStringish { }
extension Dictionary: _CollectionOrStringish { }
extension Set: _CollectionOrStringish { }
다음으로 Optional where Wrapped: _CollectionOrStringish를 확장(extension)하자.
extension Optional where Wrapped: _CollectionOrStringish {
var isNilOrEmpty: Bool {
switch self {
case let .some(value): return value.isEmpty
default: return true
}
}
}
let x: String? = ...
let y: [Int]? = ...
if x.isNilOrEmpty || y.isNilOrEmpty {
//do stuff
}
value(or:)
이것은 아주 간단하다. 이것은 함수로 표현된 ?? nil-coalescing 연산자이다.
extension Optional {
func value(or defaultValue: Wrapped) -> Wrapped {
return self ?? defaultValue
}
}
이것은 아주 코드에서 연산자의숲(operator-soup)에 들어갈때 사용하는데, 어디서 사용하든 함수형태의 것이 명확하다. 혹은 함수 파라미터로 nil-coalescing을 써야할 때 사용한다.
// operator form
if x ?? 0 > 5 {
...
}
// function form
if x.value(or: 0) > 5 {
...
}
apply(_:)
이것은 리턴 값이 없는(혹은 ()을 리턴할 수도 있다) 버전의 map이다.
extension Optional {
/// Applies a function to `Wrapped` if not `nil`
func apply(_ f: (Wrapped) -> Void) {
_ = self.map(f)
}
}
flatten()
Update: VictorPavlychoko가 댓글로 짚어주었듯, ExpressibleByNilLiteral으로 flatten을 더 간단하게 만들 수 있다!
protocol OptionalType: ExpressibleByNilLiteral { }
// Optional already has an ExpressibleByNilLiteral conformance
// so we just adopt the protocol
extension Optional: OptionalType { }
extension Optional where Wrapped: OptionalType {
func flatten() -> Wrapped {
switch self {
case let .some(value):
return value
case .none:
return nil
}
}
}
ExpressibleByNilLiteral이 적용되지 않았을 때 사용할 수 있다는 것을 설명하기 위해, 교육의 목적으로 원래의 구현을 남겨두고 있다.
원래의 flatten
이중 옵셔널로 작업해본적이 있다면 이 익스텐션의 진가를 인정할 수 있을 것이다. 여기서 몇 프로토콜과 익스텐션을 필요로 하는데, 어떤 임의의 Wrapped의 none 케이스를 구성하는 방법을 찾기위한 꼼수이다. 이 이야기가 와닫지 않는다면 축하한다. 당신에게 평범하고 생산적인 삶을 살 수 있는 희맘ㅇ이 아직 있다. 아래에다가 설명을 갈게 쪼게어 해놓았으니 보자.
- 보통 컴파일러 마법은 모든 Optional<Wrapped>들에(감쌓인것 까지도) nil을 대입하게 해주고, 그냥 모든것이 잘 동작한다.
- flatten()으로부터 리턴을 표현하기 위해 추상 타입 맴버(연관타입)을 제공할 수 있다.
* 익스텐션에서 self를 참조하고 아래처럼 제네릭 파라미터를 생략할 수 있다면
extension Optional where Wrapped: Optional
flatten() -> Wrapped.Wrapped 이렇게도 할 수 있을 것이나, 불행히도 지금 이렇게 할 수 없다. - 일반적인 옵셔널 마법은 동작하지 않아야한다. 왜냐하면 프로토콜에 익스텐션이 연관타입 WrappedType을 반환할 것이라 약속했기 때문이다. 컴파일러 마법은 nil을 .none으로 만들 수 없다.
* 만약 WrappedType: Optional<?>으로 만든다면: 동작은 할것이나 그렇게 할 수 없을 것이다.
* 만약 WrappedType: Self로 만든다면: 스스로 동작은 할 것이나 그렇게 할 수 없을 것이다.
(If we could constrain WrappedType: Optional<?> it would work but we can't.
If we could constrain WrappedType: Self it would work but we can't.) - 우리 프로토콜에서 init()를 요구조건으로 추가한다. 이것으로 WrappedType의 인스턴스를 구성하여 반환하는데 사용할 수 있다.
- OptionalType 익스텐션에서 self=nil을 사용할 수 있다. 그 이유는, 컴파일러가 self는 옵셔널이라는 것을 알고 있기 때문에 마법이 일어난다.
protocol OptionalType {
associatedtype WrappedType
init()
}
extension Optional: OptionalType {
public typealias WrappedType = Wrapped
public init() {
self = nil
}
}
extension Optional where Wrapped: OptionalType {
func flatten() -> WrappedType {
switch self {
case .some(let value):
return value
case .none:
return WrappedType()
}
}
}
언급된 몇 제약들은 결국 타입 시스템에대한 여러 증진으로 드러날 수 있다.
valueOrEmpty()
한 타입이 빈 것으로 표현될때의 작은 규약이며 이것으로 nil-coalesce하여 성가시지 않게 만들 수 있다.
/// A type that has an empty value representation, as opposed to `nil`.
public protocol EmptyValueRepresentable {
/// Provide the empty value representation of the conforming type.
static var emptyValue: Self { get }
/// - returns: `true` if `self` is the empty value.
var isEmpty: Bool { get }
/// `nil` if `self` is the empty value, `self` otherwise.
/// An appropriate default implementation is provided automatically.
func nilIfEmpty() -> Self?
}
extension EmptyValueRepresentable {
public func nilIfEmpty() -> Self? {
return self.isEmpty ? nil : self
}
}
extension Array: EmptyValueRepresentable {
public static var emptyValue: [Element] { return [] }
}
extension Set: EmptyValueRepresentable {
public static var emptyValue: Set { return Set() }
}
extension Dictionary: EmptyValueRepresentable {
public static var emptyValue: Dictionary { return [:] }
}
extension String: EmptyValueRepresentable {
public static var emptyValue: String { return "" }
}
public extension Optional where Wrapped: EmptyValueRepresentable {
/// If `self == nil` returns the empty value, otherwise returns the value.
public func valueOrEmpty() -> Wrapped {
switch self {
case .some(let value):
return value
case .none:
return Wrapped.emptyValue
}
}
/// If `self == nil` returns the empty value, otherwise returns the result of
/// mapping `transform` over the value.
public func mapOrEmpty(_ transform: (Wrapped) -> Wrapped) -> Wrapped {
switch self {
case .some(let value):
return transform(value)
case .none:
return Wrapped.emptyValue
}
}
}
descriptionOrEmpty
Swift3에서 보간법(interpolated) 문자열 옵셔널을 포함한 새로운 경고는 유용하다; 대부분 여러분은 문자열이 "(nil)"으로 표사되길 원하진 않을 것이다. 그러나 그런 동작을 원하든 아니면 그냥 빈 문자열을 원할때든 간편한 프로퍼티들이 있다.
eextension Optional {
var descriptionOrEmpty: String {
return self.flatMap(String.init(describing:)) ?? ""
}
var descriptionOrNil: String {
return self.flatMap(String.init(describing:)) ?? "(nil)"
}
}
결론
이게 유용하고 재미있었다면 이런 형식으로 임의의 익스텐션으로 몇몇 포스팅을 해왔다.
또한 이런 동작들에대한 아주 커다란 포스팅을 준비하고 있는데, 시간이 많이 걸리는 중이다. 글을 써내려가는 중이니 기다려주길 바란다.
이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은
- 블로그 댓글
- 페이스북 페이지(@나는한다번역)
- 이메일(canapio.developer@gmail.com)
- 트위터(@canapio)
으로 보내주시면 됩니다.
'Swift와 iOS > Advanced Swift' 카테고리의 다른 글
[번역]스위프트: guard와 if는 언제 사용할까 (0) | 2017.06.06 |
---|---|
[번역]스위프트에서 네이밍에 관한 것들 (2) | 2017.05.13 |
[번역]스위프트에서 세이프티 (0) | 2017.05.13 |
[번역] Optional Non-Escaping Closures (0) | 2017.03.03 |
[번역] Swift한 델리게이트 (0) | 2016.12.31 |
WRITTEN BY
- tucan.dev
개인 iOS 개발, tucan9389
,