모든 문제는 또다른 프로토콜을 추가하여 해결할 수 있다.

옵셔널은 멋지다. 이제까지 나는 Objective-C의 "messages to nil return nil" 버그를 너무 많이 봐왔었고 다시 그때로 돌아가고 싶지도 않다.

그러나 당신은 옵셔널이나 특정 타입의 옵셔널이 필요할 때가 종종 있다. 아래에는 내가 즐겨쓰는 그 경우들이다.

isNilOrEmpty
가끔씩 nilisEmpty==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
이중 옵셔널로 작업해본적이 있다면 이 익스텐션의 진가를 인정할 수 있을 것이다. 여기서 몇 프로토콜과 익스텐션을 필요로 하는데, 어떤 임의의 Wrappednone 케이스를 구성하는 방법을 찾기위한 꼼수이다. 이 이야기가 와닫지 않는다면 축하한다. 당신에게 평범하고 생산적인 삶을 살 수 있는 희맘ㅇ이 아직 있다. 아래에다가 설명을 갈게 쪼게어 해놓았으니 보자.
  1. 보통 컴파일러 마법은 모든 Optional<Wrapped>들에(감쌓인것 까지도) nil을 대입하게 해주고, 그냥 모든것이 잘 동작한다.
  2. flatten()으로부터 리턴을 표현하기 위해 추상 타입 맴버(연관타입)을 제공할 수 있다.
    * 익스텐션에서 self를 참조하고 아래처럼 제네릭 파라미터를 생략할 수 있다면
    extension Optional where Wrapped: Optional
    flatten() -> Wrapped.Wrapped 이렇게도 할 수 있을 것이나, 불행히도 지금 이렇게 할 수 없다.
  3. 일반적인 옵셔널 마법은 동작하지 않아야한다. 왜냐하면 프로토콜에 익스텐션이 연관타입 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.)
  4. 우리 프로토콜에서 init()를 요구조건으로 추가한다. 이것으로 WrappedType의 인스턴스를 구성하여 반환하는데 사용할 수 있다.
  5. 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)" 
     } 
} 

결론
이게 유용하고 재미있었다면 이런 형식으로 임의의 익스텐션으로 몇몇 포스팅을 해왔다.

또한 이런 동작들에대한 아주 커다란 포스팅을 준비하고 있는데, 시간이 많이 걸리는 중이다. 글을 써내려가는 중이니 기다려주길 바란다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,