[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 関数
前半にplayground向けの実装がありますが、今回関係あるのは後半部分です。
var output = _Stdout()
_print(items, separator: separator, terminator: terminator, to: &output)
標準出力を表す _Stdout() を生成し、 _print 関数の呼び出しに渡しています。
TextOutputStream
_Stdout() は TextOutputStream protocolに準拠する型です。
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にはロック・アンロックのメソッドとテキストを書き込むメソッドがあり、テキストの表現は String か UnsafeBufferPointer<UInt8> の2種類があります。
TextOutputStreamの抽象化によって_print は「TextOutputStream に値を出力する関数」という設計になっており、標準出力を行う print は 「(TextOutputStream の一つである) _Stdout に値を出力する関数」という建て付けになっています。この設計によって print の内部実装を任意の出力先に書き込めるように公開しています。
実際に TextOutputStream を外部から指定できるオーバーロードも存在し、例えばString はTextOutputStreamに準拠しているので、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の挿入などを行います。
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 に出力します。
- Optionalの場合→Optional.debugDescription
- Stringの場合→String(そのまま)
- TextOutputStreamableの場合→TextOutputStreamable.write
- CustomStringConvertibleの場合→ .description
- CustomDebugStringConvertibleの場合 → .debugDescription
- 上記どれにも当てはまらない場合→リフレクションを用いた形式
-
SWIFT_ENABLE_REFLECTION=NOの場合利用不可。Embedded Swiftなど。
-
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は
WrappedをdebugPrint(後述) した表現
- xxxは
- none:
"nil"
という形式になっています。
@_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"
}
}
}
ここで、debugPrintはprintとは違う優先順位でインスタンスの文字列化を行うので、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の特別対応もなし。
- CustomDebugStringConvertibleの場合 → .debugDescription
- CustomStringConvertibleの場合→ .description
- TextOutputStreamableの場合→TextOutputStreamable.write
- 上記どれにも当てはまらない場合→リフレクションを用いた形式
-
_adHocPrint_unlocked(...,isDebugPrint: true)になっており、出力が少し異なる。
-
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
A source of text-streaming operations.
Instances of types that conform to theTextOutputStreamableprotocol can write their value to instances of any type that conforms to theTextOutputStreamprotocol. The Swift standard library’s text-related types,String,Character, andUnicode.Scalar, all conform toTextOutputStreamable.
TextOutputStream に書き込めるソースであることを表すprotocol。
requirementsは一つだけで、some TextOutputStream に書き込む write メソッド。基本的にこの実装の中で target.write か target._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
A type with a customized textual representation.
Types that conform to theCustomStringConvertibleprotocol 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
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に準拠させる意味も薄いが)
なお昔は CustomStringConvertible は Printable , CustomDebugStringConvertible は DebugPrintable という名前だった。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でリフレクションを扱うための型で、リフレクションはランタイム上で動的に型・変数・プロパティ情報などを取得する仕組み。
_adHocPrint_unlocked はMirrorの情報をパースして文字列を整形していく。
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の表現が変化する。
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:)
内部で _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
Stringinstance. The initializer creates the string representation ofinstancein one of the following ways, depending on its protocol conformance:
- If
instanceconforms to theTextOutputStreamableprotocol, the result is obtained by callinginstance.write(to: s)on an empty strings.- If
instanceconforms to theCustomStringConvertibleprotocol, the result isinstance.description.- If
instanceconforms to theCustomDebugStringConvertibleprotocol, the result isinstance.debugDescription.- An unspecified result is supplied automatically by the Swift standard library.
つまり、意味的に String(describing: xxx) は以下とほぼ同義。
var string = ""
print(xxx, terminator: "", to: &string)
が、このドキュメントには嘘がある。 CustomStringConvertible と TextOutputStreamable の両方に準拠している場合、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にはこんな一言が書いてある。
The
String(describing:)initializer is the preferred way to convert an instance of any type to a string. If the passed instance conforms toCustomStringConvertible, theString(describing:)initializer and theprint(_:)function use the instance’s customdescriptionproperty.Accessing a type’s
descriptionproperty directly or usingCustomStringConvertibleas a generic constraint is discouraged.
String(describing:)イニシャライザは、あらゆる型のインスタンスを文字列に変換する推奨方法です。渡されたインスタンスがCustomStringConvertibleに準拠している場合、String(describing:)イニシャライザとprint(_:)関数は、インスタンスのカスタムdescriptionプロパティを使用します。
型のdescriptionプロパティへの直接アクセスや、CustomStringConvertibleをジェネリック制約として使用することは推奨されません。
任意のインスタンスを文字列化したい場合は、(CustomStringConvertible は文字列化手段の一つでしかないので、より汎用的な)複数のアプローチから文字列化を行う上フォールバックもある _print_unlocked に一本化して使ってねということだと思う。
String(reflecting:)
debugPrint 関数が String(reflecting:)に対応する。
extension String {
public init<Subject>(reflecting subject: Subject) {
self.init()
_debugPrint_unlocked(subject, &self)
}
}
DefaultStringInterpolation
StringInterpolationのデフォルト挙動である DefaultStringInterpolation も内部で _print_unlocked を呼ぶ。つまりprintとロジックを共有している。
StringInterpolationについて詳しくはこちら
@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