🖨️

[Swift] printの出力形式の挙動を深掘る

に公開

はじめに

Swiftには標準出力を行う Swift.print があります。
Swiftを書いたことがある人は全員使ったことがあるでしょう。

print("Hello World!!") // Hello World!!

このprint関数の引数の型はAny (の可変長引数)なので、文字列に限らず色々な型を入力し標準出力することができます。

public func print(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n"
)
print(10) // 10
print(1.2345) // 1.2345
print(Optional(10)) // Optional(10)

struct Counter {
    var count: Int
}
print(Counter(count: 10)) // Counter(count: 10)

enum Hoge {
    case a
}
print(Hoge.a) // a

このように入力の型によって出力形式が適切に選択されていることがわかります。

printはこれらの形式をどこから参照しているのでしょうか。どうやって任意の型を文字列に変換しているのでしょうか。
この記事ではprintの出力形式・挙動と関連するSwiftのprotocolを深掘って解説します。

print 関数

https://github.com/swiftlang/swift/blob/0f75a3a69e0bd6e182aba383ad998c72e3696235/stdlib/public/core/Print.swift#L54-L68

前半にplayground向けの実装がありますが、今回関係あるのは後半部分です。

var output = _Stdout()
_print(items, separator: separator, terminator: terminator, to: &output)

標準出力を表す _Stdout() を生成し、 _print 関数の呼び出しに渡しています。

TextOutputStream

_Stdout()TextOutputStream protocolに準拠する型です。

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

public protocol TextOutputStream {
  mutating func _lock()
  mutating func _unlock()

  /// Appends the given string to the stream.
  mutating func write(_ string: String)

  mutating func _writeASCII(_ buffer: UnsafeBufferPointer<UInt8>)
}

requirementsにはロック・アンロックのメソッドとテキストを書き込むメソッドがあり、テキストの表現は StringUnsafeBufferPointer<UInt8> の2種類があります。

TextOutputStreamの抽象化によって_print は「TextOutputStream に値を出力する関数」という設計になっており、標準出力を行う print は 「(TextOutputStream の一つである) _Stdout に値を出力する関数」という建て付けになっています。この設計によって print の内部実装を任意の出力先に書き込めるように公開しています。

実際に TextOutputStream を外部から指定できるオーバーロードも存在し、例えばStringTextOutputStreamに準拠しているので、Stringへの書き込みに利用できます。

public func print<Target: TextOutputStream>(
  _ items: Any...,
  separator: String = " ",
  terminator: String = "\n",
  to output: inout Target
) {
  _print(items, separator: separator, terminator: terminator, to: &output)
}

var string = ""
print(10, terminator: "", to: &string) // stringに出力する。標準出力ではない

string // "10"

_print 関数

_print 関数では可変長引数を展開して _print_unlocked 関数を必要回呼び出します。更にこの関数で出力先のロック, separator/terminatorの挿入などを行います。

https://github.com/swiftlang/swift/blob/0f75a3a69e0bd6e182aba383ad998c72e3696235/stdlib/public/core/Print.swift#L219-L234

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)
}

_print_unlocked 関数

_print_unlocked 関数が出力処理のコアロジックです。複数のアプローチからインスタンスの文字列化を試みます。

以下の順序で入力 value をチェックし、最初にマッチした形式を使用して TextOutputStream に出力します。

  1. Optionalの場合→Optional.debugDescription
  2. Stringの場合→String(そのまま)
  3. TextOutputStreamableの場合→TextOutputStreamable.write
  4. CustomStringConvertibleの場合→ .description
  5. CustomDebugStringConvertibleの場合 → .debugDescription
  6. 上記どれにも当てはまらない場合→リフレクションを用いた形式
    1. SWIFT_ENABLE_REFLECTION=NO の場合利用不可。Embedded Swiftなど。

https://github.com/swiftlang/swift/blob/0f75a3a69e0bd6e182aba383ad998c72e3696235/stdlib/public/core/OutputStream.swift#L409-L451

internal func _print_unlocked<T, TargetStream: TextOutputStream>(
  _ value: T, _ target: inout TargetStream
) {
  // Optional has no representation suitable for display; therefore,
  // values of optional type should be printed as a debug
  // string. Check for Optional first, before checking protocol
  // conformance below, because an Optional value is convertible to a
  // protocol if its wrapped type conforms to that protocol.
  // Note: _isOptional doesn't work here when T == Any, hence we
  // use a more elaborate formulation:
  if _openExistential(type(of: value as Any), do: _isOptional) {
    let debugPrintable = value as! CustomDebugStringConvertible
    debugPrintable.debugDescription.write(to: &target)
    return
  }

  if let string = value as? String {
    target.write(string)
    return
  }

  if case let streamableObject as TextOutputStreamable = value {
    streamableObject.write(to: &target)
    return
  }

  if case let printableObject as CustomStringConvertible = value {
    printableObject.description.write(to: &target)
    return
  }

  if case let debugPrintableObject as CustomDebugStringConvertible = value {
    debugPrintableObject.debugDescription.write(to: &target)
    return
  }

#if SWIFT_ENABLE_REFLECTION
  let mirror = Mirror(reflecting: value)
  _adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
#else
  target.write("(value cannot be printed without reflection)")
#endif
}

なお、冒頭に例示したサンプルコードの各出力がどのケースに当てはまるかを示すと以下のようになります。

print(10) // 10 4.CustomStringConvertible.description
print(1.2345) // 1.2345 3.TextOutputStreamable.write
print(Optional(10)) // Optional(10) 1.Optional.debugDescription

struct Counter {
    var count: Int
}
print(Counter(count: 10)) // Counter(count: 10) 6.リフレクションを用いた形式

enum Hoge {
    case a
}
print(Hoge.a) // a 6.リフレクションを用いた形式

以下、各条件について詳しく見ていきます。

1. Optionalの場合→Optional.debugDescription

  // Optional has no representation suitable for display; therefore,
  // values of optional type should be printed as a debug
  // string. Check for Optional first, before checking protocol
  // conformance below, because an Optional value is convertible to a
  // protocol if its wrapped type conforms to that protocol.
  // Note: _isOptional doesn't work here when T == Any, hence we
  // use a more elaborate formulation:
  if _openExistential(type(of: value as Any), do: _isOptional) {
    let debugPrintable = value as! CustomDebugStringConvertible
    debugPrintable.debugDescription.write(to: &target)
    return
  }

まず、冒頭にOptionalの特別対応があります。

Optionalの場合 Optional.debugDescription が固定で利用されます。これは _print_unlocked は後続処理で入力にキャストを行い各protocolへの準拠を確認しますが、入力がOptionalの場合、Swiftのキャストの仕組みが原因でOptional自体のconformanceを確認できないためです。

protocol AProtocol {}
struct A: AProtocol {}

func test<T>(_ t: T) {
    // TはOptional<A>であり、Optional<A>自体はAProtocolに準拠していない
    // しかし、以下のキャストは成功する
    if let aProtocol = t as? AProtocol {
        print("AProtocol")
    } else {
        print("else")
    }
}

let aOptional: A? = A()
test(aOptional)

また、上記の制約に加え、Optionalは既存のdebugDescription以外に妥当な出力が無いため、debugDescriptionに固定しても良いと判断されたことがコメントから読み取れます。

Optional.debugDescription

Optional.debugDescription

  • some: "Optional(xxx)"
    • xxxはWrappeddebugPrint (後述) した表現
  • none: "nil"

という形式になっています。

https://github.com/swiftlang/swift/blob/0f75a3a69e0bd6e182aba383ad998c72e3696235/stdlib/public/core/Optional.swift#L457-L479

@_unavailableInEmbedded
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("
      #if !$Embedded
      debugPrint(value, terminator: "", to: &result)
      #else
      _ = value
      "(cannot print value in embedded Swift)".write(to: &result)
      #endif
      result += ")"
      return result
#else
    return "(optional printing not available)"
#endif
    case .none:
      return "nil"
    }
  }
}

ここで、debugPrintprintとは違う優先順位でインスタンスの文字列化を行うので、print(Optional(A()))print(A())において、A部分の表現も変わる可能性があります。

struct Counter {
    var count = 0
}
extension Counter: CustomStringConvertible {
    var description: String {
        "CustomStringConvertible"
    }
}
extension Counter: CustomDebugStringConvertible {
    var debugDescription: String {
        "CustomDebugStringConvertible"
    }
}

print(Counter()) // CustomStringConvertible
print(Optional(Counter())) // Optional(CustomDebugStringConvertible)

debugPrint

実態は CustomDebugStringConvertible が優先される print で、以下の優先順位で利用されます。OptionalやStringの特別対応もなし。

  1. CustomDebugStringConvertibleの場合 → .debugDescription
  2. CustomStringConvertibleの場合→ .description
  3. TextOutputStreamableの場合→TextOutputStreamable.write
  4. 上記どれにも当てはまらない場合→リフレクションを用いた形式
    • _adHocPrint_unlocked(...,isDebugPrint: true)になっており、出力が少し異なる。

https://github.com/swiftlang/swift/blob/78608b372266f1afda473e98aed0bf95c6dfd28b/stdlib/public/core/OutputStream.swift#L456-L462

2. Stringの場合→String(そのまま)

入力がStringの場合、Stringがそのまま利用されます。

TextOutputStream.write の入力もStringなので、特別な処理が必要ないhappy-pathです。

  if let string = value as? String {
    target.write(string)
    return
  }

3. TextOutputStreamableの場合→TextOutputStreamable.write

  if case let streamableObject as TextOutputStreamable = value {
    streamableObject.write(to: &target)
    return
  }

TextOutputStreamable

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

A source of text-streaming operations.
Instances of types that conform to the TextOutputStreamable protocol can write their value to instances of any type that conforms to the TextOutputStream protocol. The Swift standard library’s text-related types, StringCharacter, and Unicode.Scalar, all conform to TextOutputStreamable.

TextOutputStream に書き込めるソースであることを表すprotocol。

requirementsは一つだけで、some TextOutputStream に書き込む write メソッド。基本的にこの実装の中で target.writetarget._writeASCII を呼ぶことが期待されます。

func write<Target: TextOutputStream>(to target: inout Target)

例: Floatの実装

extension Float: TextOutputStreamable {
  public func write<Target>(to target: inout Target) where Target: TextOutputStream {
    var buffer = _InlineArray<64, UTF8.CodeUnit>(repeating: 0x30)
    var span = buffer.mutableSpan
    let textRange = _Float32ToASCII(value: self, buffer: &span)
    let textStart = unsafe span._start().assumingMemoryBound(to: UTF8.CodeUnit.self) + textRange.lowerBound
    let textLength = textRange.upperBound - textRange.lowerBound

    let textBuff = unsafe UnsafeBufferPointer<UTF8.CodeUnit>(_uncheckedStart: textStart,
                                                             count: textLength)
    unsafe target._writeASCII(textBuff)
  }
}

なぜ浮動小数点だけ TextOutputStreamable への準拠が存在する理由は見つけられなかったが、おそらく文字列化処理のOutputが UnsafeBufferPointer<UInt8> なので、TextOutputStream._writeASCII を直接使うことでString化のコストをカットできるメリットがあるからだと推測した。

このように、TextOutputStream に書き込む際のコスト上のメリットや、書き込み前後に差し込みたい処理がある場合にTextOutputStreamable に準拠させるのが良さそう。

4. CustomStringConvertibleの場合→ .description

  if case let printableObject as CustomStringConvertible = value {
    printableObject.description.write(to: &target)
    return
  }

CustomStringConvertible

https://developer.apple.com/documentation/Swift/CustomStringConvertible

A type with a customized textual representation.
Types that conform to the CustomStringConvertible protocol can provide their own representation to be used when converting an instance to a string.

インスタンスがカスタマイズされた表現のStringに変換できることを示す。

public protocol CustomStringConvertible {
  var description: String { get }
}

例えば点を表すPointに (0, 1) のような表現を指定できる。

struct Point {
    let x: Int, y: Int
}
extension Point: CustomStringConvertible {
    var description: String {
        return "(\(x), \(y))"
    }
}

5. CustomDebugStringConvertibleの場合 → .debugDescription


  if case let debugPrintableObject as CustomDebugStringConvertible = value {
    debugPrintableObject.debugDescription.write(to: &target)
    return
  }

CustomDebugStringConvertible

https://developer.apple.com/documentation/Swift/CustomDebugStringConvertible

A type with a customized textual representation suitable for debugging purposes.

インスタンスがデバッグ向けにカスタマイズされた表現のStringに変換できることを示す。

構造的には CustomStringConvertible と同じ。

public protocol CustomDebugStringConvertible {
  var debugDescription: String { get }
}

CustomStringConvertible との使い分けだが、Setの例を見るとdebugDescriptionでは型情報を追加するなどしている。

print(Set([1, 2, 3]).description)      // [1, 2, 3]
print(Set([1, 2, 3]).debugDescription) // Set([1, 2, 3])

ただ、例えばArrayなどはどちらのprotocolでも同じ表現を返しているので、同じでも良さそう。(まぁそれだと両方のprotocolに準拠させる意味も薄いが)

なお昔は CustomStringConvertiblePrintable , CustomDebugStringConvertibleDebugPrintable という名前だった。print関数の挙動をカスタマイズする目的で導入されたので Printable という命名にしたが、実態としては文字列表現を提供する汎用的な物なので、実態に沿った名前にしたということだと思う。

6. 上記どれにも当てはまらない場合→リフレクションを用いた形式

#if SWIFT_ENABLE_REFLECTION
  let mirror = Mirror(reflecting: value)
  _adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)
#else
  target.write("(value cannot be printed without reflection)")
#endif

上記1~5に当てはまらなかった場合のフォールバック。自作型をprintしたときに見る機会が多いCounter(count: 10) みたいな表現。

Mirror を生成し _adHocPrint_unlocked に渡す。

let mirror = Mirror(reflecting: value)
_adHocPrint_unlocked(value, mirror, &target, isDebugPrint: false)

Mirror はSwiftでリフレクションを扱うための型で、リフレクションはランタイム上で動的に型・変数・プロパティ情報などを取得する仕組み。

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

_adHocPrint_unlocked はMirrorの情報をパースして文字列を整形していく。

https://github.com/swiftlang/swift/blob/78608b372266f1afda473e98aed0bf95c6dfd28b/stdlib/public/core/OutputStream.swift#L302

if let displayStyle = mirror.displayStyle {
    switch displayStyle {
      case .optional: ...
      case .tuple: ...
      case .struct:
        printTypeName(mirror.subjectType)
        target.write("(")
        var first = true
        for (label, value) in mirror.children {
          if let label = label {
            if first {
              first = false
            } else {
              target.write(", ")
            }
            target.write(label)
            target.write(": ")
            _debugPrint_unlocked(value, &target)
          }
        }
        target.write(")")
      case .enum: ...

CustomReflectable

Mirror(reflecting:)CustomReflectable によって任意の表現に差し替えることができるため、このprotocolの準拠の有無でもprintの表現が変化する。

https://github.com/swiftlang/swift/blob/7bc31084b64e56acdc6f265e8433bb357f018c9d/stdlib/public/core/Mirror.swift#L70-L76

public struct Mirror {
  public init(reflecting subject: Any) {
    if case let customized as CustomReflectable = subject {
      self = customized.customMirror
    } else {
      self = Mirror(internalReflecting: subject)
    }
  }
}

struct Counter {
    var count: Int
    var _value: Int
}

extension Counter: CustomReflectable {
    var customMirror: Mirror {
        Mirror(
            self,
            children: [("count", count)], // _valueは隠す
            displayStyle: .struct
        )
    }
}

print(Counter(count: 10, _value: 20)) // Counter(count: 10)

おまけ: printに関連する他API

String(describing:)

https://developer.apple.com/documentation/swift/string/init(describing:)-67ncf

https://github.com/swiftlang/swift/blob/0f75a3a69e0bd6e182aba383ad998c72e3696235/stdlib/public/core/Mirror.swift#L584-L587

内部で _print_unlocked を呼ぶ。つまりprintとロジックを共有している。

extension String {
  public init<Subject>(describing instance: Subject) {
    self.init()
    _print_unlocked(instance, &self)
  }
}

実はこの記事で解説したprintの(大まかな)挙動は String(describing:) のドキュメントコメントに書いてある。

Use this initializer to convert an instance of any type to its preferred representation as a String instance. The initializer creates the string representation of instance in one of the following ways, depending on its protocol conformance:

  • If instance conforms to the TextOutputStreamable protocol, the result is obtained by calling instance.write(to: s) on an empty string s.
  • If instance conforms to the CustomStringConvertible protocol, the result is instance.description.
  • If instance conforms to the CustomDebugStringConvertible protocol, the result is instance.debugDescription.
  • An unspecified result is supplied automatically by the Swift standard library.

つまり、意味的に String(describing: xxx) は以下とほぼ同義。

var string = ""
print(xxx, terminator: "", to: &string)

が、このドキュメントには嘘がある。 CustomStringConvertibleTextOutputStreamable の両方に準拠している場合、String(describing: xxx)CustomStringConvertible を優先する。

struct Counter {
    var count = 1
}
extension Counter: CustomStringConvertible {
    var description: String {
        "CustomStringConvertible"
    }
}

extension Counter: TextOutputStreamable {
    func write<Target>(to target: inout Target) where Target : TextOutputStream {
        target.write("TextOutputStreamable")
    }
}

print(String(describing: Counter())) // "CustomStringConvertible"
print(Counter()) // "TextOutputStreamable"

実はString(describing: xxx) にはオーバーロードがあり、入力が CustomStringConvertible または TextOutputStreamable に準拠している場合、 _print_unlocked を呼び出さない。入力が両方のprotocolに準拠している場合、 TextOutputStreamable ではなく CustomStringConvertible の実装を使っているので、_print_unlocked と挙動が統一されていない。

  @inlinable
  public init<Subject: CustomStringConvertible>(describing instance: Subject) {
    self = instance.description
  }

  @inlinable
  public init<Subject: TextOutputStreamable>(describing instance: Subject) {
    self.init()
    instance.write(to: &self)
  }

  @inlinable
  public init<Subject>(describing instance: Subject)
    where Subject: CustomStringConvertible & TextOutputStreamable
  {
    self = instance.description
  }

CustomStringConvertibleの使い方

また、CustomStringConvertibleにはこんな一言が書いてある。

https://developer.apple.com/documentation/Swift/CustomStringConvertible

The String(describing:) initializer is the preferred way to convert an instance of any type to a string. If the passed instance conforms to CustomStringConvertible, the String(describing:) initializer and the print(_:) function use the instance’s custom description property.

Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.

String(describing:) イニシャライザは、あらゆる型のインスタンスを文字列に変換する推奨方法です。渡されたインスタンスが CustomStringConvertible に準拠している場合、String(describing:) イニシャライザと print(_:) 関数は、インスタンスのカスタム description プロパティを使用します。
型の description プロパティへの直接アクセスや、CustomStringConvertible をジェネリック制約として使用することは推奨されません。

任意のインスタンスを文字列化したい場合は、(CustomStringConvertible は文字列化手段の一つでしかないので、より汎用的な)複数のアプローチから文字列化を行う上フォールバックもある _print_unlocked に一本化して使ってねということだと思う。

String(reflecting:)

https://developer.apple.com/documentation/swift/string/init(reflecting:)

debugPrint 関数が String(reflecting:)に対応する。

extension String {
  public init<Subject>(reflecting subject: Subject) {
    self.init()
    _debugPrint_unlocked(subject, &self)
  }
}

DefaultStringInterpolation

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

StringInterpolationのデフォルト挙動である DefaultStringInterpolation も内部で _print_unlocked を呼ぶ。つまりprintとロジックを共有している。

StringInterpolationについて詳しくはこちら

https://zenn.dev/kntk/articles/3802535b76605a#expressiblebystringinterpolation

  @inlinable
  public mutating func appendInterpolation<T>(_ value: T) {
    #if !$Embedded
    _print_unlocked(value, &self)
    #else
    "(cannot print value in embedded Swift)".write(to: &self)
    #endif
  }

つまり "\(xxx)" も意味的には以下とほぼ同義。

var string = ""
print(xxx, terminator: "", to: &string)

また、 String(describing:) と同様にオーバーロードがあり、入力が CustomStringConvertible または TextOutputStreamable に準拠している場合、 _print_unlocked を呼び出さない。なお両方のprotocolに準拠している場合、こちらは TextOutputStreamable の実装を利用しているので、_print_unlockedと挙動が統一されており純粋なhappy-pathとして動作する。

  @inlinable
  public mutating func appendInterpolation<T>(_ value: T)
    where T: TextOutputStreamable, T: CustomStringConvertible
  {
    value.write(to: &self)
  }
  
  @inlinable
  public mutating func appendInterpolation<T>(_ value: T)
    where T: TextOutputStreamable
  {
    value.write(to: &self)
  }

  @inlinable
  public mutating func appendInterpolation<T>(_ value: T)
    where T: CustomStringConvertible
  {
    value.description.write(to: &self)
  }

依存概略図

Discussion