Swift/개념 & 응용

[Swift] Optional.swift 살펴보기

유정주 2023. 8. 17. 10:12
반응형

Optional.swift를 살펴보게 된 계기

옵셔널은 Swift의 장점 중 하나입니다.

 

백준 문제를 풀다가 문득 nil 병합 연산자(??)에 대해 궁금해졌습니다.

일반적인 연산자는 즉시 연산이 완료되는데, ?? 연산자는 앞의 수행 결과가 nil인 경우 실행이 되는 부분이 흥미로웠어요.

어떻게 구현되었길래 지연 실행이 되는거지? 라는 생각이 들었습니다.

 

많은 블로그에서 이미 다룬 주제지만, 실제 구현 코드가 궁금해서 Optional.swift를 살펴봤는데요.

보기 전에는 너무 어려워서 못 읽을 줄 알았지만 막상 보니 읽을만 한거에요..?

그래서 한 번 쫙 읽고 포스팅으로 정리까지 하게 되었습니다 ㅋㅋ

전부 다루는건 아니기 때문에 Optional.swift와 함께 보시면 좋습니다.

 

 

기본 구조

@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)

    public init(_ some: Wrapped) { self = .some(some) }
  
    public init(nilLiteral: ()) {
        self = .none
    }
}

Optional은 Enum으로 구현되어 있습니다.

 

case

옵셔널 Enum의 케이스는 두 가지입니다.

nil을 의미하는 none, nil이 아닌 값인 some입니다.

some은 Generic 타입 연관값을 사용하고 있습니다.

 

init

init(_ some: Wrapped)는 nil이 아닌 값일 때 호출됩니다.

매개변수로 제네릭 타입을 전달 받고 그 값을 연관값으로 사용하여 인스턴스를 생성합니다.

 

nil은 none 케이스로 인스턴스를 생성합니다.

init(nilLiteral:)에서 nil로 초기화가 되는데요.

이 메서드는 ExpressibleByNilLiteral의 구현 메서드입니다.

 

ExpressibleByNilLiteral은 nil로 초기화할 수 있음을 의미하는 프로토콜입니다.

ExpressibleByNilLiteral 프로토콜을 준수해야 nil로 초기화가 가능한거죠.

옵셔널은 이 프로토콜을 준수하고 있기 때문에

var a: Int? = nil

위 코드처럼 nil로 초기화가 가능한 것입니다.

 

ExpressibleByNilLiteral은 옵셔널 타입만 준수하고 있습니다.

즉, 옵셔널 타입이 아니라면 nil로 초기화할 수 없습니다.

var n = nil //compile error

따라서 위 코드처럼 옵셔널 타입으로 추론이 불가능하다면 nil로 초기화할 수 없습니다.

 

참고로 ExpressibleByNilLiteral는 Nil 뿐만 아니라 타입마다 존재합니다.

그냥 한 화면에 보이는 것만 가져왔는데도 상당하죠?

흥미 있으신 분은 공식 문서로 가서 검색해보시면 재밌을 거 같네요 ㅋㅋ

 

@frozen

마지막으로 옵셔널 enum은 frozen 속성(attribute)을 사용합니다.

@frozen은 더이상 enum에 case를 추가하지 않겠다는 걸 의미하기 때문에 

switch문에서 default를 사용하지 않아도 됩니다.

 

 

출력

다음은 출력 부분입니다.

 

CustomDebugStringConvertible

extension Optional: CustomDebugStringConvertible {
  /// A textual representation of this instance, suitable for debugging.
  public var debugDescription: String {
    switch self {
    case .some(let value):
#if !SWIFT_STDLIB_STATIC_PRINT
      var result = "Optional("
      debugPrint(value, terminator: "", to: &result)
      result += ")"
      return result
#else
    return "(optional printing not available)"
#endif
    case .none:
      return "nil"
    }
  }
}

옵셔널 타입을 출력하면 Optional(OOO)으로 나오잖아요?

그걸 담당하는 코드입니다.

 

nil이 아닌 경우, Optaion( + value + ) 로 출력을 해주는데

Swift의 standard library print 문이 아니라면 optional printing not available 이라고 출력합니다.

(stdlib print가 아닌 경우는 아직 모르겠습니다만 오늘은 옵셔널이 주제이므로 여기선 넘어가겠습니다 ㅎㅎ;)

 

nil인 경우 nil 이라고 출력합니다.

 

CustomReflectable

#if SWIFT_ENABLE_REFLECTION
extension Optional: CustomReflectable {
  public var customMirror: Mirror {
    switch self {
    case .some(let value):
      return Mirror(
        self,
        children: [ "some": value ],
        displayStyle: .optional)
    case .none:
      return Mirror(self, children: [:], displayStyle: .optional)
    }
  }
}
#endif

옵셔널은 reflection도 지원합니다.

reflection이란 런타임에 동적으로 타입의 멤버를 검사하는 동작인데요.

Swift에서는 타입에 있는 저장 프로퍼티의 값을 읽을 수 있다고 합니다.

 

customMirror에서는 Mirror 인스턴스를 생성해서 반환합니다.

Mirror는 특정 인스턴스의 타입, 그 인스턴스의 하위 값의 정보를 가지고 있습니다.

옵셔널이 nil이 아니면 value를 포함한 딕셔너리를, nil인 경우 빈 딕셔너리를 optional 스타일로 생성하고 있네요.

참고로 displayStyle을 사용하는 코드는 DebuggerSupport.swift에서 찾을 수 있었습니다.

private static func asStringRepresentation(
    value: Any?,
    mirror: Mirror,
    count: Int
) -> String? {
    switch mirror.displayStyle {
    case .optional? where count > 0:
        return "\(mirror.subjectType)"
    case .optional?:
      return value.map(String.init(reflecting:))
    ...
}

 

 

 

타입 정보에 따라 다르게 출력이 된다는 것을 알 수 있었습니다.

 

 

고차 함수

옵셔널에는 map, flatMap이 구현되어 있습니다.

@inlinable
public func map<U>(
    _ transform: (Wrapped) throws -> U
) rethrows -> U? {
    switch self {
    case .some(let y):
      return .some(try transform(y))
    case .none:
      return .none
    }
}
@inlinable
public func flatMap<U>(
    _ transform: (Wrapped) throws -> U?
) rethrows -> U? {
    switch self {
    case .some(let y):
      return try transform(y)
    case .none:
      return .none
    }
}

둘의 차이점은 some 케이스 처리에 있습니다.

map은 .some으로 한 번 wrapping해서 옵셔널 인스턴스로 반환하지만,

flatMap은 연산 결과를 바로 반환합니다. (flatMap이 옵셔널을 벗기는 원리가 이유가 이거군요!)

 

참고로 inlinable 속성은 컴파일 시기에 메서드 호출 코드를 메서드 본문 코드로 대체할 수 있음을 의미합니다.

메서드 호출이 줄어들어서 최적화가 되는 원리입니다.

Swift에만 존재하는게 아니라 일반적인 프로그래밍 언어 개념이기 때문에 알아두시면 좋습니다.

WWDC에서 제네릭 최적화에 대해 다룰 때도 인라인 개념이 나왔으니 이것도 알아두시면 좋습니다 ㅎㅎ

 

 

Equatable

옵셔널은 Wrapped 타입이 Equatable인 경우, Equatable 연산을 지원합니다.

extension Optional: Equatable where Wrapped: Equatable {
  @_transparent
  public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
      return l == r
    case (nil, nil):
      return true
    default:
      return false
    }
  }
}

두 개의 Wrapped? 타입 매개변수를 비교해서

둘 다 nil이 아니면 값을, 둘 다 nil이면 true를, 둘 중 하나만 nil이면 false를 반환합니다.

!= 연산은 ==를 ! 연산으로 사용하기 때문에 개발자가 따로 구현하지 않아도 됩니다.

 

하지만 실제로 사용해보면 Wrapped 타입이 Equatable이 아니어도 nil과 비교가 가능하죠?

이 경우는 따로 구현이 되어 있고 그 내용은 아래에서 다룹니다.

 

 

Hashable

extension Optional: Hashable where Wrapped: Hashable {
  @inlinable
  public func hash(into hasher: inout Hasher) {
    switch self {
    case .none:
      hasher.combine(0 as UInt8)
    case .some(let wrapped):
      hasher.combine(1 as UInt8)
      hasher.combine(wrapped)
    }
  }
}

Hashable도 Wrapped가 Hashable일 때만 지원합니다.

 

Hashable은 Equatable과 달리 예외가 존재하지 않습니다.

Wrapped 타입이 Hashable하지 않으면 Wrapped 타입 값으로 해시를 생성할 수 없기 때문입니다.

 

nil인 경우 0으로 해시값을 생성하고, nil이 아닌 경우 1과 wrapped 연관값으로 해시값을 생성합니다.

이때 combine은 해시 함수입니다.

이와 관련해선 Hashable 포스팅을 참고해 주세요.

 

궁금한 점

근데 0, 1을 사용하는 이유는 모르겠습니다 ;; 정확한 정보가 없더라고요.

추측만 해보면... nil이 0을 이용해 해시값을 생성하는데, some의 wrapped가 0일 경우도 있잖아요?

그래서 해시 충돌을 방지하기 위해 1과 wrapped 두 개로 해시 값을 생성하는게 아닐까 싶네요.

정확히 아시는 분이 계시다면 꼭 좀 알려주시면 감사하겠습니다.

 

 

_OptionalNilComparisonType

_OptionalNilComparisonType는 Equatable이 아닌 타입도 nil 연산을 할 수 있도록 구현한 구조체입니다.

공식적인 문서는 없지만, 주석과 내부 구현 코드를 보면 합리적인 추측이라는 것을 알 수 있습니다.

// Enable pattern matching against the nil literal, even if the element type
// isn't equatable.
@frozen
public struct _OptionalNilComparisonType: ExpressibleByNilLiteral {
  /// Create an instance initialized with `nil`.
  @_transparent
  public init(nilLiteral: ()) {
  }
}

Optional Enum과 달리 init(nilLiteral:) 본문이 비어있습니다.

옵셔널 nil로 초기화 하는 것이 아니라는 것을 알 수 있습니다.

 

이 구조체는 ~=, ==, != 연산자 메서드의 매개 변수 타입으로 사용됩니다.

extension Optional {
  @_transparent
  public static func ~=(lhs: _OptionalNilComparisonType, rhs: Wrapped?) -> Bool {
    switch rhs {
    case .some:
      return false
    case .none:
      return true
    }
  }
  
  @_transparent
  public static func ==(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {
    switch lhs {
    case .some:
      return false
    case .none:
      return true
    }
  }

  @_transparent
  public static func !=(lhs: Wrapped?, rhs: _OptionalNilComparisonType) -> Bool {
    switch lhs {
    case .some:
      return true
    case .none:
      return false
    }
  }

  @_transparent
  public static func ==(lhs: _OptionalNilComparisonType, rhs: Wrapped?) -> Bool {
    switch rhs {
    case .some:
      return false
    case .none:
      return true
    }
  }

  @_transparent
  public static func !=(lhs: _OptionalNilComparisonType, rhs: Wrapped?) -> Bool {
    switch rhs {
    case .some:
      return true
    case .none:
      return false
    }
  }
}

Equatable을 사용하는게 아니라 직접 연산자 메서드를 구현하는 것이므로 == 이외의 ~=, !=도 구현되어 있습니다.

또한, some의 연관값을 사용하지 않고 nil인지 아닌지만 판단하고 있고,

==, != 연산에서 발생할 수 있는 모든 시나리오를 오버 로딩되어 있습니다.

 

이 구현 덕분에 저희가 Equatable이 아닌 타입도 nil인지 아닌지 비교 연산을 할 수 있는 것입니다.

당연히 되는 연산이 아니었다는게 놀랍습니다 ㅎㅎ

 

 

nil 병합 연산자(??)

다음은 ?? 연산자 입니다.

제일 궁금한 내용이었는데 거의 마지막에 나오네요.

@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

?? 연산자는 제네릭 메서드이고, defaultValue가 @autoclosure로 선언되어 있습니다.

optional 값이 nil인 경우, defaultValue를 사용하는데 그 모습이 약간 특이합니다.

마치 함수처럼 defaultValue 뒤에 ( )가 붙어 있죠?

왜냐하면 defaultValue에 @autoclosure 속성이 붙었기 때문입니다.

 

@autoclosure는 호출부에서 클로저처럼 사용하지 않더라도, 자동으로 클로저로 만들어주는 속성입니다.

그리고 autoclosure로 만들어진 클로저는 기본적으로 지연 실행이 됩니다.

?? 연산자 앞의 결과를 기다렸다가 none인 경우 defaultValue 클로저가 실행되는 것입니다.

 

@_transparent
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

참고로 ?? 연산자는 반환값이 옵셔널인 버전과 옵셔널이 아닌 버전이 있습니다.

anyFunction() ?? 1 //defaultValue가 옵셔널이 아님
anyFunction() ?? Int(1) //defaultValue가 옵셔널임

그래서 위 코드는 옵셔널 여부에 따라 결과가 달라집니다.

 

 

Sendable

마지막으로 Sendable입니다.

extension Optional: Sendable where Wrapped: Sendable { }

Sendable은 Value 타입인 경우 채택만 하면 되기 때문에 단 한 줄이네요 ㅋㅋ

Value 타입의 장점을 여기서도 볼 수 있었습니다.

 

 

Bridging

나머지는 Objective-C Bridging 코드입니다.

//===----------------------------------------------------------------------===//
// Bridging
//===----------------------------------------------------------------------===//

#if _runtime(_ObjC)
extension Optional: _ObjectiveCBridgeable {
   ...
}

내부 코드는 살펴보지 않았지만 이렇게 Bridging이라고 구분선을 작성해준 게 인상적이었습니다.

역시 대기업의 주석답다라는 걸 느낄 수 있었어요 ㅋㅋ

 

 

마무리

Optional.swift 살펴보기가 마무리 되었습니다.

 

?? 연산자만 보고 도망갔으면 후회했을 것이라 느낄 정도로 알찼는데

포스팅을 읽는 여러분께도 제대로 전달이 되었을진 모르겠습니다 ㅎㅎ;

 

Swift에서 옵셔널은 핵심적인 내용인데, 취준생따리인 제가 읽을 수 있을 정도로 쉬운 코드였다는게 놀라웠습니다.

핵심 코드 == 어렵다 라는 편견이 깨진 경험이었네요.

 

또다른 궁금증이 생겨 오픈소스를 읽을 기회가 된다면 꼭 다시 포스팅하겠습니다 ㅎㅎ

 

감사합니다!

 

 

참고

https://github.com/apple/swift/blob/main/stdlib/public/core/Optional.swift

https://developer.apple.com/documentation/swift/optional

https://developer.apple.com/documentation/swift/expressiblebynilliteral

https://velog.io/@wonhee010/Optional-Code

https://stackoverflow.com/questions/33209540/swift-optional-type-how-none-nil-works


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형