Swift
swift4

Stringのinit(describing:)について

Swiftの勉強中なので、調べたり試したりしたことを備忘的に書きます。

なお、実行環境はmacOS High Sierra (10.13.3)、Xcode 9.2 (9C40b)のiOS Playgroundです。

init(describing:)が返す文字列の決定ルール

SwiftのStringのイニシャライザに、以下のようなものがあります。

init<Subject>(describing instance: Subject)

こいつは任意の型Subjectの内容をパパッと文字列化するのに便利なイニシャライザのようです。
APIリファレンスによると、このイニシャライザによって生成される文字列表現は以下のようなルールによって決まるようです。

  1. instanceTextOutputStreamableプロトコルに準拠する場合、空文字列sに対してinstance.write(to: s)を呼び出すことで結果が得られる。
  2. instanceCustomStringConvertibleプロトコルに準拠する場合、結果はinstance.descriptionとなる。
  3. instanceCustomDebugStringConvertibleプロトコルに準拠する場合、結果はinstance.debugDescriptionになる。
  4. Swift 標準ライブラリによって自動的に結果が与えられる。

ルールは1、2、3、4の順に適用されるようです。以下、試してみました。

ルール適用順序の確認

どのプロトコルにも準拠しない場合

class Person {
    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let p = Person(name: "John", age: 34)
print(String(describing: p))

// 出力
// __lldb_expr_1.Person

ルール4が適用されるため、Swift標準ライブラリが生成した"__lldb_expr_1.Person"という結果となります。

CustomDebugStringConvertibleに準拠した場合

class Person: CustomDebugStringConvertible {
    var debugDescription: String {
        return "DEBUG: Person(name: \(self.name), age: \(self.age))"
    }

    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let p = Person(name: "John", age: 34)
print(String(describing: p))

// 出力
// DEBUG: Person(name: John, age: 34)

ルール3が適用されます。

CustomStringConvertibleにも準拠した場合

class Person: CustomDebugStringConvertible, CustomStringConvertible {
    var description: String {
        return "Person(name: \(self.name), age: \(self.age))"
    }

    var debugDescription: String {
        return "DEBUG: Person(name: \(self.name), age: \(self.age))"
    }

    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let p = Person(name: "John", age: 34)
print(String(describing: p))

// 出力
// Person(name: John, age: 34)

ルール2が優先されるため、descriptionプロパティの値が返されます。

TextOutputStreamableにも準拠する場合

class Person: CustomDebugStringConvertible, CustomStringConvertible, TextOutputStreamable
{
    func write<Target>(to target: inout Target) where Target : TextOutputStream {
        target.write("STREAMABLE: \(self.description)")
    }

    var description: String {
        return "Person(name: \(self.name), age: \(self.age))"
    }

    var debugDescription: String {
        return "DEBUG: Person(name: \(self.name), age: \(self.age))"
    }

    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let p = Person(name: "John", age: 34)
print(String(describing: p))

// 出力
// STREAMABLE: Person(name: John, age: 34)

ルール1が優先されるため、write(to:)の結果が返されます。

NSObjectについて

NSObjectCustomStringConvertibleCustomDebugStringConvertibleに準拠しています。
descriptionプロパティとdebugDescriptionプロパティが返す文字列は同じです。

let nso = NSObject()
print(String(describing: nso))
print(nso.description)
print(nso.debugDescription)

// 出力
// <NSObject: 0x60400001c7e0>
// <NSObject: 0x60400001c7e0>
// <NSObject: 0x60400001c7e0>

したがって、NSObjectを継承するクラスのインスタンスをString(describing)に渡して、インスタンスの内容を出力させたい場合は以下のいずれかの方策をとることになるでしょう。

descriptionプロパティのオーバーライド

class NSPerson : NSObject {
    override var description: String {
        return "Person(name: \(self.name), age: \(self.age))"
    }

    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

// 出力
// Person(name: Cathy, age: 26)

名前、年齢を出力させることができました。

TextOutputStreamableに準拠させる

class NSPerson : NSObject, TextOutputStreamable {
    func write<Target>(to target: inout Target) where Target : TextOutputStream {
        target.write("Person(name: \(self.name), age: \(self.age))")
    }

    var name: String
    var age: Int
    init (name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let nsp = NSPerson(name: "Cathy", age: 26)
print(String(describing: nsp))

// 出力
// Person(name: Cathy, age: 26)

名前、年齢を出力させることができました。

まとめ

  • String(describing:)にインスタンスを渡すことで、そのインスタンスの情報を文字列で取得することができる。
  • String(describing:)が返す文字列の決定ルールとして、渡されたインスタンスがTextOutputStreamableCustomStringConvertibleCustomDebugStringConvertibleの順に、各プロトコルに準拠しているかチェックされる。
  • 準拠しているプロトコルが見つかれば、そのプロトコルのメソッドやプロパティが返す文字列が返される。
  • いずれのプロトコルにも準拠していない場合、Swift標準ライブラリが提供する文字列表現が返される。