Swift

Swift | Optional(value) 원론적으로 왜 출력될까?

ziziDev 2024. 7. 1. 10:35
반응형

 

 

Swift | Optional에 관하여

안녕하세요!오늘은 타언어에서는 보지 못한 옵셔널에 대해서 알아보고자 합니다 옵셔널 옵셔널은 값을 반환하는 과정에서 오류가 발생하는 상황이 많기 때문에안정성을 높이고자 사용합니다

ios2080dev.tistory.com

 

옵셔널 이전 정리를 보면

optional은 열거형으로 이루어져 있으며

case가 some, none으로 이루어져 있습니다

 

 

일반 자료형보다 큰 범위의 자료형이라고 보면 됩니다

 

 

왜 Optional(3)으로 출력이 되는지 동작 과정에 대해서 알아보고자 합니다

 

우선 Optional은 case some / none으로 이루어져있는 코드에 대해서 알아보고자 합니다

 

optioanl type을 정의하고 있으며 복사 불가능한 타입(~Copyable)과 관련된 제한을 포함하고 있습니다

Optioanl type은 Wrapped라는 제네릭타입을 가지고 있습니다

어떤 타입이라도 래핑할 수 있습니다

 

그리고 컴파일러가 optioanl<Wrapped>를 최적화나 어떠한 처리를 할 수 있습니다

 

var int: Optional<Int> = .none //값이 없음
int = .some(42) //값이 존재

print(int) //Optional(42)

var optionalInt: Int? = nil //.none == nil
optionalInt = 42 //.some(42) 동일함

 

extension Optional: ExpressibleByNilLiteral where Wrapped: ~Copyable {
  /// Creates an instance initialized with `nil`.
  ///
  /// Do not call this initializer directly. It is used by the compiler when you
  /// initialize an `Optional` instance with a `nil` literal. For example:
  ///
  ///     var i: Index? = nil
  ///
  /// In this example, the assignment to the `i` variable calls this
  /// initializer behind the scenes.
  @_transparent
  @_preInverseGenerics
  public init(nilLiteral: ()) {
    self = .none
  }
}
//복사 불가능한 경우에만 적용되는 상황
//nil 리터럴을 사용해서 Optioanl을 none으로 초기화하는 메서드

 

 

optioanl정의문에서

none으로 초기화하는걸 볼 수 있습니다

 

 public var unsafelyUnwrapped: Wrapped {
    @inline(__always)
    get {
      if let x = self {
        return x
      }
      _debugPreconditionFailure("unsafelyUnwrapped of nil optional")
    }
  }
}

 

 

그리고 언래핑을 사용하여

안정성 거사를 생략하고 강제로 추출하는것을 볼 수 있습니다

nil인 경우에 오류 발생하는것을 볼 수 있습니다

 

//복사 불가능할 때 적용 
//개발자가 강제로 nil을 벗겨낼 때 발생하는 예외상황에 대해서 처리하는 문구도 볼 수 있습니다
extension Optional where Wrapped: ~Copyable {
  // FIXME(NCG): Do we want this? It seems like we do. Make this public.
  @_alwaysEmitIntoClient
  public consuming func _consumingUnsafelyUnwrap() -> Wrapped {
    switch consume self {
    case .some(let x):
      return x
    case .none:
      _debugPreconditionFailure("consumingUsafelyUnwrap of nil optional")
    }
  }
}

extension Optional {
  /// - Returns: `unsafelyUnwrapped`.
  ///
  /// This version is for internal stdlib use; it avoids any checking
  /// overhead for users, even in Debug builds.
  @inlinable
  internal var _unsafelyUnwrappedUnchecked: Wrapped {
    @inline(__always)
    get {
    	//if let 바인딩을 통해서 안전성을 검사하고 값이 없을 경우에 오류 발생 
      if let x = self {
        return x
      }
      _internalInvariantFailure("_unsafelyUnwrappedUnchecked of nil optional")
    }
  }
}

//복사 불가능 / if let 바인딩을 통하여 안전한지 검사하고 값이 존재하지 않는경우에 오류 발생 
extension Optional where Wrapped: ~Copyable {
  /// - Returns: `unsafelyUnwrapped`.
  ///
  /// This version is for internal stdlib use; it avoids any checking
  /// overhead for users, even in Debug builds.
  @_alwaysEmitIntoClient
  internal consuming func _consumingUncheckedUnwrapped() -> Wrapped {
    if let x = self {
      return x
    }
    _internalInvariantFailure("_uncheckedUnwrapped of nil optional")
  }
}

 

이 코드들 같은 경우 각기 다른 변환 메서드를 보여주고 있습니다

 

그외에도 비교 메서드 등을 참고 할 수 있습니다

👇🏻👇🏻👇🏻👇🏻👇🏻

https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L121

 

swift/stdlib/public/core/Optional.swift at main · swiftlang/swift

The Swift Programming Language. Contribute to swiftlang/swift development by creating an account on GitHub.

github.com

 

본론으로 돌아와

왜 Optioanl을 프린트할 때 Optioanl로 보여지는가?

답은 CustomDebugStringConvertible 여기서 볼 수 있습니다

 

 

CustomDebugStringConvertible 프로토콜을 채택하여 확장을 통하여 Debug시에 Optional 타입일 때

"Optional(" + ')"을 출력하도록 지정하고 있습니다

 

개발자들에게 Optional 타입을 가시적으로 명확하게 알려줄 수 있는 역할을 하고 있는것을 볼 수 있습니다

 

 

여기서 CustomDebugStringConvertible 프로토콜을 추가적으로 알아보자면

 

 

읽기 전용 프로퍼티를 사용하여 디버그용 문자열을 반환하고있는걸 볼 수 있습니다

주로 객체 상태를 명확하게 알 수 있습니다

 

struct Apple: CustomDebugStringConvertible {

	var name: String
    var version: Float
    var price: Int
    var debugDescription: String { 
    		return "Apple name \(name), version \(version), price \(price)
        }
}

 

debugDescription을 반드시 채택하여 구체화 사용해야합니다

프로토콜을 읽기전용으로 사용하지만

채택했을 때 읽기 쓰기로 get/set으로 확장도 가능합니다

 

그리고 print도 보자면

여러가지 오버로딩으로 이루어진 함수입니다

//===--- Print.swift ------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if !SWIFT_STDLIB_STATIC_PRINT

/// Writes the textual representations of the given items into the standard
/// output.
///
/// You can pass zero or more items to the `print(_:separator:terminator:)`
/// function. The textual representation for each item is the same as that
/// obtained by calling `String(describing: item)`. The following example
/// prints a string, a closed range of integers, and a group of floating-point
/// values to standard output:
///
///     print("One two three four five")
///     // Prints "One two three four five"
///
///     print(1...5)
///     // Prints "1...5"
///
///     print(1.0, 2.0, 3.0, 4.0, 5.0)
///     // Prints "1.0 2.0 3.0 4.0 5.0"
///
/// To print the items separated by something other than a space, pass a string
/// as `separator`.
///
///     print(1.0, 2.0, 3.0, 4.0, 5.0, separator: " ... ")
///     // Prints "1.0 ... 2.0 ... 3.0 ... 4.0 ... 5.0"
///
/// The output from each call to `print(_:separator:terminator:)` includes a
/// newline by default. To print the items without a trailing newline, pass an
/// empty string as `terminator`.
///
///     for n in 1...5 {
///         print(n, terminator: "")
///     }
///     // Prints "12345"
///
/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
public func print(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
) {
  if let hook = _playgroundPrintHook {
    var output = _TeeStream(left: "", right: _Stdout())
    _print(items, separator: separator, terminator: terminator, to: &output)
    hook(output.left)
  }
  else {
    var output = _Stdout()
    _print(items, separator: separator, terminator: terminator, to: &output)
  }
}

/// Writes the textual representations of the given items most suitable for
/// debugging into the standard output.
///
/// You can pass zero or more items to the
/// `debugPrint(_:separator:terminator:)` function. The textual representation
/// for each item is the same as that obtained by calling
/// `String(reflecting: item)`. The following example prints the debugging
/// representation of a string, a closed range of integers, and a group of
/// floating-point values to standard output:
///
///     debugPrint("One two three four five")
///     // Prints "One two three four five"
///
///     debugPrint(1...5)
///     // Prints "ClosedRange(1...5)"
///
///     debugPrint(1.0, 2.0, 3.0, 4.0, 5.0)
///     // Prints "1.0 2.0 3.0 4.0 5.0"
///
/// To print the items separated by something other than a space, pass a string
/// as `separator`.
///
///     debugPrint(1.0, 2.0, 3.0, 4.0, 5.0, separator: " ... ")
///     // Prints "1.0 ... 2.0 ... 3.0 ... 4.0 ... 5.0"
///
/// The output from each call to `debugPrint(_:separator:terminator:)` includes
/// a newline by default. To print the items without a trailing newline, pass
/// an empty string as `terminator`.
///
///     for n in 1...5 {
///         debugPrint(n, terminator: "")
///     }
///     // Prints "12345"
///
/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
public func debugPrint(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
) {
  if let hook = _playgroundPrintHook {
    var output = _TeeStream(left: "", right: _Stdout())
    _debugPrint(items, separator: separator, terminator: terminator, to: &output)
    hook(output.left)
  }
  else {
    var output = _Stdout()
    _debugPrint(items, separator: separator, terminator: terminator, to: &output)
  }
}

/// Writes the textual representations of the given items into the given output
/// stream.
///
/// You can pass zero or more items to the `print(_:separator:terminator:to:)`
/// function. The textual representation for each item is the same as that
/// obtained by calling `String(describing: item)`. The following example
/// prints a closed range of integers to a string:
///
///     var range = "My range: "
///     print(1...5, to: &range)
///     // range == "My range: 1...5\n"
///
/// To print the items separated by something other than a space, pass a string
/// as `separator`.
///
///     var separated = ""
///     print(1.0, 2.0, 3.0, 4.0, 5.0, separator: " ... ", to: &separated)
///     // separated == "1.0 ... 2.0 ... 3.0 ... 4.0 ... 5.0\n"
///
/// The output from each call to `print(_:separator:terminator:to:)` includes a
/// newline by default. To print the items without a trailing newline, pass an
/// empty string as `terminator`.
///
///     var numbers = ""
///     for n in 1...5 {
///         print(n, terminator: "", to: &numbers)
///     }
///     // numbers == "12345"
///
/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
///   - output: An output stream to receive the text representation of each
///     item.
public func print<Target: TextOutputStream>(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  _print(items, separator: separator, terminator: terminator, to: &output)
}

/// Writes the textual representations of the given items most suitable for
/// debugging into the given output stream.
///
/// You can pass zero or more items to the
/// `debugPrint(_:separator:terminator:to:)` function. The textual
/// representation for each item is the same as that obtained by calling
/// `String(reflecting: item)`. The following example prints a closed range of
/// integers to a string:
///
///     var range = "My range: "
///     debugPrint(1...5, to: &range)
///     // range == "My range: ClosedRange(1...5)\n"
///
/// To print the items separated by something other than a space, pass a string
/// as `separator`.
///
///     var separated = ""
///     debugPrint(1.0, 2.0, 3.0, 4.0, 5.0, separator: " ... ", to: &separated)
///     // separated == "1.0 ... 2.0 ... 3.0 ... 4.0 ... 5.0\n"
///
/// The output from each call to `debugPrint(_:separator:terminator:to:)`
/// includes a newline by default. To print the items without a trailing
/// newline, pass an empty string as `terminator`.
///
///     var numbers = ""
///     for n in 1...5 {
///         debugPrint(n, terminator: "", to: &numbers)
///     }
///     // numbers == "12345"
///
/// - Parameters:
///   - items: Zero or more items to print.
///   - separator: A string to print between each item. The default is a single
///     space (`" "`).
///   - terminator: The string to print after all items have been printed. The
///     default is a newline (`"\n"`).
///   - output: An output stream to receive the text representation of each
///     item.
public func debugPrint<Target: TextOutputStream>(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  _debugPrint(items, separator: separator, terminator: terminator, to: &output)
}

internal func _print<Target: TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  var prefix = ""
  output._lock()
  defer { output._unlock() }
  for item in items {
    output.write(prefix)
    _print_unlocked(item, &output)
    prefix = separator
  }
  output.write(terminator)
}

internal func _debugPrint<Target: TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  var prefix = ""
  output._lock()
  defer { output._unlock() }
  for item in items {
    output.write(prefix)
    _debugPrint_unlocked(item, &output)
    prefix = separator
  }
  output.write(terminator)
}

#endif

 

코드를 뭉치를 나눠서 설명하자면

public func print(
  _ items: Any..., //가변 인자 출력할 객체들
  separator: String = " ", //기본값 공백 ""은 공백 사라짐
  terminator: String = "\n" //줄바꿈
)

//⭐️프린트 메서드와 유사함 하지만 디버깅용
public func debugPrint(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
)

 

//TextOutputStream protocol 각각 인자들을 문자열로 반환하여 출력함
internal func _print<Target: TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
)

internal func _debugPrint<Target: TextOutputStream>(
  _ items: [Any],
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
)

 

결론적으로

 

Optioanl(value)가 출력되는 과정은 여러 단계를 거쳐 나오는걸 알 수 있습니다

단순히 print를 통해 나오는게 아니라

Optional의 extension에서 protocol- CustomDebugStringConvertible을 통해서

switch로 값의 유무를 판단하고 출력되는 과정을 볼 수 있었습니다

그리고 출력(print 함수)도 오버로딩 기능을 활용해서 구현된 것을 볼 수 있었습니다

 

 

Optioanl에 대해서 더 알고싶다면 (아래 세부 항목을 하나하나 클릭하면서 보시면 자세히 알 수 있습니다)

👇🏻👇🏻👇🏻👇🏻👇🏻

 

Optional | Apple Developer Documentation

A type that represents either a wrapped value or the absence of a value.

developer.apple.com

 

 

반응형