'옵셔널'에 해당하는 글 3건

제목: That One Optional Property

때론 새로운 것을 위해 여러분의 뷰컨트롤러를 수정해야할 필요가 있다. 한가지는 다른것을 야기하는데, 뷰컨트롤러에 옵셔널 변수를 추가하는 자신을 발견할 것이다. 이것은 몇몇 케이스에서 설정될 것이고 몇몇은 아닐 것이다.

내 생각엔 더 자주 일어나고 이것은 결점이있는 방법이다. 여기에는 몇가지 이유가 있다. 첫째로 어떨때만 쓰이는 옵셔널 프로퍼티를가진 클래스는 정체성이 약한 느낌을 가진다. 바꿔 말해보면, 옵셔널 프로퍼티를 추가하면 그 타입의 근본적인 의미가 흐려진다. 첫번째로 이 옵셔널 프로퍼티는 어떤 시멘틱 의미를 고려하지 않는다. 이 타입이 nil이라면 이 오브젝트의 상태가 무엇이라 할 수 있을까? 여러분의 코드를 가볍게 읽고있는 사람들은 어떤 경우에 프로퍼티가 nil이될지, 혹은 그 오브젝트에서 가지는 분기가 무엇인지 말할 수 없을 것이다. 세번째로 코드는 가변으로 계속 될것이다. 한 옵셔널 프로퍼티는 누가지 선택적 프로퍼티로 될것이고, 이것은 세개로 되고, 당신이 알기 전에 미끄러운 경사의 아래에 가있을 것이다. 이 값이 반드시 존재하는지 이 값이 nil인지 표현하고 싶으면 간단한 옵셔널로는 그렇게 할 수 없다.

내가본 모든 코드베이스에서는, 여러분의 뷰컨트롤러에 왜 옵셔널 프로퍼티가 필요한지에대한 두가지 주된 이유를 발견했다. 나는 이 두가지 다를 탐험할 것이고 각 문제를 해결하기위한 더 나은 패턴을 제안할 것이다.

문제의 옵셔널 프로퍼티를 가지는것에대한 첫번째 이유는 이 뷰컨트롤러가 쓰일때마다 오브젝트나 몇 데이터를 반드시 가질 필요가 없을때이다.

최근에 만난 이것에대한 예시는 뷰컨트롤러가 어떤 경우 푸시 노티피케이션으로부터 표시되는 상황이다. 이때 노티피케이션에서 나온 특정 메시지를 보여줘야한다. 이 문제를 해결하기위한 가장 간단한 방법은 옵셔널 문자열 프로퍼티를 추가하는 것이었다.
class LocationViewController: UIViewController {
     //...
     var notificationMessage: String?
     //...
}
뷰컨트롤러에 다른 코드는 어떤 뷰에 할당할지, 어떻게 배치할지등을 결정하기위해 메시지가 있다고 전환했다. 프로퍼티의 선택성은 그냥 문자열의 존재보다 더 표현한다. It had implications in the rest of the view controller and the rest of the view layer.


여기서 더 중요한 점은 문자열이 더이상 거기에 있을지 아니면 없을지 표현하는게 아니게 된다. 이제 뷰컨트롤러 안에있는 표현 스타일이나 모드를 표현한다. 이것이 푸시 노티피케이션의 문맥에서 만들어진 것인가 혹은 일반 브라우징을 통해 만들어진 것인가? 답은 가까스로 관련된 프로퍼티에 있다.

이 문제를 해결하기위해, 이 모드를 모두 명시적으로 만들어야한다. 이 뷰컨트롤러에서 이것은 일급시민이면, 뷰컨트롤러의 그 영향은 더 분명해질것이다.
class LocationViewController: UIViewController {
     //...
     enum Mode {
          case fromNotification(message: String)
          case normal
     }

     var mode: Mode
     //...
}
Sandi Metz가 말한것처럼, 한 특수화는 절때 없다(there's never one specialization). 옵셔널 프로퍼티를 쓰면 그 코드는 이 프로퍼티의 nil 상태에대한 의미를 가지거나 고유의 의미를 가진다고 할 수 없다. 열거형을 사용하면 코드에서 구체화되고 형식화된다.

새로운 열겨형이 Optional 타입 정의의 열거형 형태와 아주 비슷하다는 점을 인지하자.
enum Optional<Wrapped> {
     case some(Wrapped)
     case none
}
그러나 여기에는 몇가지 분명히 다른 유용한점이 존재한다.

첫째로 시멘틱이다. somenone은 추상적이다. normalfromNotification은 그것과 관련된 의미를 가진다. 여러분의 코드를 읽는 사람들은 여러분에게 감사할 것이다.

두전째로 확장성이다. 뷰 컨트롤러에 다른 모드가 추가되면 그것을 완전히 설명하는 더나은 의미를 가진다. 만약 새로운 모드가 두 모드와 절때 겹치지 않으면, 필요한 연관된 데이터가 어떻든 새로운 열거형 케이스를 추가할 수 있다. 새로운 모드가 현재 모드들과 겹친다면, 새로운 열거형이되어 새로운 프로퍼티를 함께할 수 있다. 오브젝트의 상태는 읽기에 더욱 설명가능한 것이 된다. 어떤 선택이든 새 모드를 위해 또다른 옵셔널을 추가하는 것보다는 낫다.

세번째도 확장성이다. LaunchViewController.Mode가 일급 타입이기 때문에 우리는 함수와 계산된 프로퍼티를 추가할 수 있다. 예를들어 노티피케이션 메시지를 잡아두는 레이블의 높이는 아마 그 메시지의 존재에 달렸을 것이다. 따라서 코드를 열거형으로 옮길 수 있다.
extension Mode {
     var notificationLabelHeight: CGFloat {
          switch self {
          case .normal: return 0
          case .fromNotification(let message): return message.size().height
          }
     }
}
더 풍요로운 타입으로 데이터를 옮기는 것은 적은 코드를 투자하여 이 모든 앞단에서의 이득을 제공한다.

옵셔널 프로퍼티를 사용할지도 모르는 두번째 이유는 첫번째 이유의 부분집합이다. 어떤 경우, 옵셔널 프로퍼티는 뷰컨트롤러의 다른 모드를 표현하지 않는데, 코드에서 임시적인 특징을 표현한다. 아직 값을 가지고 있지 않기 때문에 값으로 프로퍼티를 초기화할 수 없다. 이것의 일반적인 예시는 네트워크로부터 뭔가 패치를 하거나, 시스템으로부터 뭔가 긴 검색시간이 걸려 비동기가 필요해서 기다려야 할때이다.
class UserViewController: UIViewController {
     //...

     // will be loaded asynchronously
     var user: User?
     //...
}
이런 종류의 문제는 어떨때 데이터가 배열형식으로 들어오면 준비될 수 있다. 빈 배열로 존재하지 않는 상태를 표현할 수 있기 때문에, 옵셔널을 사용하지 않더라도 이 문제는 여전히 숨어있다. 테이블 뷰 컨트롤러에 빈 상태를 추가하는 힘든 상황의 횟수가 바로 이런 문제의 정도를 나타낸다.

이 문제는 첫번째 문제의 부분집합이기 때문에, 열거형으로 같은 방법으로 해결할 수 있다.
enum LoadingState<Wrapped> {
     case initialcase loading
     case loaded(Wrapped)
     case error(Error)
}
이 솔루션이 동작하는동안, 비동기 상태를 관리하기위한 더 나은 추상화가 있다. 아직 존재하진 않지만 미래의 어느 시점엔 있을 한 값은 Promise에의해 잘 표현된다. 프로미스는 여러분의 유스케이스에따라 여러 이점을 가진다.

먼저, 프로미스는 가변을 허락하지만, 오직 한번만 그리고 오직 결정되지 않을때까지만이다. 프로미스가 한번 변경되면, 다시는 변경할 수 없다. (여러번에걸처 변경해야한다면, Signal이나 Observable이 여러분이 찾던 것일 것이다.) 이 말은 let과같은 시멘틱을 가지지만 여전히 비동기 데이터를 관리한다.

다음으로, 프로미스는 값이 들어왔을때, 없어질 블럭을 추가하는 기능을 가진다. 만약 프로퍼티가 간단한 옵셔널 프로퍼티를 남긴다면, 추가된 블럭들은 didSet 프로퍼티 옵저버에서 코드와 동일하다. 그러나 프로미스 블럭은 하나 이상 추가할 수 있고, 클래스 어디에서든 추가될 수 있기 때문에 어욱 강력하다. 게다가 프로미스가 이미 값으로 채워져 있을때 추가한다면 그들은 즉시 실행될것이다.

마짐가으로 필요에따라 프로미스는 에러도 처리한다. 특정 종류의 비동기 데이터에대해, 이것이 중요하며, 이것으로부터 자유로울 것이다.

Promise 라이브러리를 사용하고 있다면 빈 생성자로 미결정된 Promise를 생성할 수 있고, fulfill 메소드로 언제든지 채울 수 있다.

옵셔널 프로퍼티를 사용할 때는, 가끔 예전에 보이지 않던 것들이 드러낸다. 뷰컨트롤러에 옵셔널 파라미터를 추가하는 자신을 발견할 때 스스로에게 물어보자. 이 옵셔널이 정말로 의미하는바는 무엇인가? 이 데이터를 표현할 더 좋은 방법은 없는가?



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

으로 보내주시면 됩니다.


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

,
제목: Pretty much every way to assign optionals

Non-옵셔널을 저장하거나 옵셔널을 옵셔널에 저장하기

기본적인 것이다. 로켓과학처럼 복잡한 것이 아니다.
optItem = 5 // optItem is now .some(5)
optItem = optValue // optItem is whatever optValue is
요약
사용빈도: 모든 경우
터무니없는 접근법인가: 전혀 그렇지 않다.

옵셔널을 non-옵셔널에 저장하기
많은 함수와 메소드들이 옵셔널 값을 반환한다. throw와 함께 try?를 사용할때도 옵셔널 값을 반환한다. 여러분은 종종 그 결과를 non-옵셔널 변수 혹은 프로퍼티로 담아둬야 할 때가 있을 것이다.

이렇게하기위해 nil을 테스트하고 어떤 non-nil 결과의 언랩핑된 결과를 저장한다. 아래에 몇가지 방법이 있다.

조건부의 바인딩
if let을 사용하여 조건부로 옵셔널을 바인딩하고 대입을 실행할 수 있다.
if let optItem = optItem { 
    item = optItem // if optItem is non-nil
}
if let과 완전히 동일하지만 그래도 여러분이 원한다면 if case도 사용할 수 있다.
// Sugared optional
if case let optItem? = optItem { 
    item = optItem // if optItem is non-nil
}

// External let
if case let .some(optItem) = optItem {
    item = optItem // if optItem is non-nil
}

// Internal let
if case .some(let optItem) = optItem { 
    item = optItem // if optItem is non-nil 
}

Nil coalescing
만일의 대비의 값으로 nil coalescing을 사용할 수 있다.
item = optItem ?? fallbackValue
만일의 대비의 값이 필요없다면 원래 값을 사용해도 된다.
item = optItem ?? item
약간의 주의. 컴파일러가 "자신에 자신을 할당하는" 경우를 최적화하는지 잘 모르겠다. 그것을 확인하고 할당한다면 if let 방법보다 덜 효율적일 것이다.

또한 이 Itemnon-nil 일때만 갱신한다는 의도가 추가적인 if let을 쓰는게 깔끔하다고 생각하므로 효율면이나 가독성면에서 이것을 추천하진 않는다.

요약
사용빈도: 종종 사용한다
터무니없는 접근법인가: 전혀 아니다

optItemnon-nil일때만 옵셔널을 갱신하기
이번에는 현재 옵셔널이 nil일때 갱신을 스킵하는 시나리오에대해 설명하겠다. 이러한 경우 nil은 "이 옵셔널을 건드리지 마시오"라는 의미이다. 나는 이런 시나리오가 일어나지 않게 생각했다.

명확한 해결 방법이다.
if optItem != nil { optItem = newValue }
? 표시를 사용한 완전히 이상한 벙법이다.
optItem? = nonOptionalValue
?의 사용에서 rhs는 반드시 non-옵셔널이어야하고 컴파일 시점에 보장된다. 스위프트 언어의 특징중에 다소 모호한 것이다(이것을 짚어준 Joe Groff에게 감사하다).

혹은 "non-nil 수신자를 위한 테스트" 대입을 위해 이렇게 할 수 있다(바보같지만).
if let optValue = optValue {
    optItem? = optValue
}
이 예제는, rhs? 대입은 non-옵셔널이여야한다. 조건부로 옵셔널을 바인딩하는 것은 ?을 쓸 수 있게 해준다. Madalin Sava가 아래의 간단한 대안을 알려주었다. (이 섹션의 모든 것이 그렇듯) 절약 면에서는 높은 점수지만 명료하지 않은 결과에대해서는 낮은 점수를 받는다.
optItem? = optValue ?? optItem!
요약
사용빈도: 절대 사용하지 말라
터무니없는 접근법인가: 확실히 그렇다

optItemnil일때만 옵셔널을 갱신하기
이번에는 "대부분 사용하는, 한번만 세팅하기"로 설명할 수 있겠다. 옵셔널이 non-nil 값으로 한번 대입하게 되면, 그것은 다시 덮어쓰이지 않게 만든다. 대입을 하기 전에 nil을 확인하여 가장 간단하게 할 수 있다.
if optItem == nil { optItem = newValue }

혹은 오퍼레이터로 불태워도 된다. 아마 이렇게 하고 싶진 않을 것이다.

infix operator =?? : AssignmentPrecedence

// "fill the nil" operator
public func =??<T>(target: inout T?, newValue: T?) {
    if target == nil { target = newValue }
}
optItem =?? newValue

이것도 한번 보자: SE-0024

요약
사용빈도: 나는 이렇게 사용하진 않으나, 다시 대입되는 것을 막고 싶을때 유용할 것이다. 이것이 묵시적으로 언랩핑된 옵셔널의 미친 버전의 종류이나, 그것을 다시는 바꾸지 않는다고 보장함을 테스트하는 곳과, (IVO의) 모든 일련의 변화가 non-nil 값이여야하는 곳이 아닌..
터무니없는 접근법인가: 터무니없지는 않으나 일반적이지도 않다.

새로운 값이 non-nil일때만 옵셔널을 갱신하기
이 시나리오는 기본적으로 묵시적인 언랩핑된 옵셔널을 따라한 것이지만 세이프티가 추가되고 IVO 크레쉬가 없다. 항상 non-nil에대한 테스트를 하기때문에 그 값을 검증하여 한번 세팅하면 옵셔널이 다시는 nil을 반환하지 않을 것이다.

한계는 non-nil의 새 값을 갱신하고 nil 대입을 버린다.
if let newValue = newValue { optItem = newValue }

혹은 이렇게(nil-값을 위해 다시 대입하는 행위를 한다는게 낭비처럼 느껴지지 않는가?)
optItem = newValue ?? optItem
혹은 연산자로 불태워도 된다. 마찬가지로 이렇게 하고 싶지는 않을 것이다.
infix operator =? : AssignmentPrecedence

// "assign non-nil values" operator
public func =?<T>(target: inout T, newValue: T?) {
    if let newValue = newValue {
        target = unwrapped
    }
}
이것도 한번 보자 : Swift Evolution

요약
사용빈도: 내가 사용하진 않는다만 non-IVO 옵셔널을 사용하고 싶은 사람들이 원할 수도 있을 것 같다.
터무니없는 접근법인가: 그렇진 않으나 일반적이지도 않다.


'Swift와 iOS > Swift Basic' 카테고리의 다른 글

[번역]No-contiguous raw value enumeration  (372) 2017.05.13

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

,

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

옵셔널은 멋지다. 이제까지 나는 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

,