Stringのinit(describing:)について


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

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

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

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

```Swift
init<Subject>(describing instance: Subject)
```

こいつは任意の型`Subject`の内容をパパッと文字列化するのに便利なイニシャライザのようです。
[APIリファレンス](https://developer.apple.com/documentation/swift/string/2427941-init)によると、このイニシャライザによって生成される文字列表現は以下のようなルールによって決まるようです。

1. `instance`が[TextOutputStreamable](https://developer.apple.com/documentation/swift/textoutputstreamable)プロトコルに準拠する場合、空文字列`s`に対して`instance.write(to: s)`を呼び出すことで結果が得られる。
1. `instance`が[CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible)プロトコルに準拠する場合、結果は`instance.description`となる。
1. `instance`が[CustomDebugStringConvertible](CustomDebugStringConvertible)プロトコルに準拠する場合、結果は`instance.debugDescription`になる。
1. Swift 標準ライブラリによって自動的に結果が与えられる。

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

# ルール適用順序の確認

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

```Swift
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に準拠した場合

```Swift
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にも準拠した場合

```Swift
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にも準拠する場合

```Swift
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について

[NSObject](https://developer.apple.com/documentation/objectivec/nsobject)は[CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible)、[CustomDebugStringConvertible](CustomDebugStringConvertible)に準拠しています。
`description`プロパティと`debugDescription`プロパティが返す文字列は同じです。

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

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

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

* `description`プロパティをオーバーライドする
* [TextOutputStreamable](https://developer.apple.com/documentation/swift/textoutputstreamable)に準拠させる

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

```Swift
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`に準拠させる

```Swift
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:)`が返す文字列の決定ルールとして、渡されたインスタンスが`TextOutputStreamable`、`CustomStringConvertible`、`CustomDebugStringConvertible`の順に、各プロトコルに準拠しているかチェックされる。
* 準拠しているプロトコルが見つかれば、そのプロトコルのメソッドやプロパティが返す文字列が返される。
* いずれのプロトコルにも準拠していない場合、Swift標準ライブラリが提供する文字列表現が返される。