Swift で値の文字列表現に関わる主なプロトコルをまとめます。
ExpressibleByStringLiteral
以下のように定義されているプロトコルです。
public protocol ExpressibleByStringLiteral : ExpressibleByExtendedGraphemeClusterLiteral {
associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral
init(stringLiteral value: Self.StringLiteralType)
}
StringLiteralType は String もしくは StaticString のどちらかです。多くの場合は String を使用すると思います。
ExpressibleByStringLiteral プロトコルに準拠すると変数や定数を文字列リテラルで初期化できます。
以下の例では、User の id を UserId 型にしつつ文字列リテラルで初期化可能にしています。
struct UserId {
let value: String
}
extension UserId: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = UserId(value: value)
}
}
struct User {
let id: UserId
// ...
}
let user1 = User(id: "taro")
print(user1)
let userId = UserId(value: "taro")
let user2 = User(id: userId)
print(user1)
let id = "taro"
let user3 = User(id: id) // Error
CustomStringConvertible
以下のように定義されているプロトコルです。
public protocol CustomStringConvertible {
var description: String { get }
}
print で表示する際の形式をカスタマイズできます。また、String(describing:) は description を利用して文字列を生成します。print は String(describing:) を利用します。
String(describing:) はプロトコルの準拠によって以下のいずれかの方法でで生成する文字列を決定します。
- CustomStringConvertible に準拠していれば description の値
- TextOutputStreamable に準拠してれば write された値
- CustomDebugStringConvertible に準拠していれば debugDescription の値
- 上記プロトコルに準拠、Swift standard library により自動生成された値
優先順位は明示的にリファレンスには記述されていないのですが、
- print では
- TextOutputStreamable > CustomStringConvertible > CustomDebugStringConvertible
- String(:describing:) では
- CustomStringConvertible > CustomDebugStringConvertible > TextOutputStreamable
でした。
以下の例では、User の文字列表現を description で指定しています。
struct User {
let id: String
let name: String
}
extension User: CustomStringConvertible {
var description: String {
return "\(name) [id=\(id)]"
}
}
let user = User(id: "123", name: "taro")
print(user) // -> taro [id=123]
CustomDebugStringConvertible
以下のように定義されているプロトコルです。
public protocol CustomDebugStringConvertible {
var debugDescription: String { get }
}
debugPrint で表示する際の形式をカスタマイズできます。
また、String(reflecting:) が生成する文字列が debugDescription になります。String(reflecting:) は値の詳細な表現を返すイニシャライザです。
String(reflecting:) はprotocol が準拠されているかどうかにより以下の方法で文字列を決定します。
- CustomDebugStringConvertible に準拠していれば debugDescription の値
- CustomStringConvertible に準拠していれば description の値
- TextOutputStreamable に準拠してれば write された値
- 上記プロトコルに準拠し絵地なければ、Swift standard library が自動生成した値
優先順位はリファレンスには明記されていないのですが、試してみると debugPrint も String(refrecting:) も 1, 2, 3 の順でした。
debugDescription プロパティを直接利用することは推奨されていません。
以下の例では、debugPrint の出力を debugDescription で指定しています。
struct User {
let id: String
let name: String
}
extension User: CustomDebugStringConvertible {
var debugDescription: String {
return "\(name), \(id)"
}
}
let user = User(id: "123", name: "taro")
debugPrint(user)
LosslessStringConvertible
以下のように定義されているプロトコルです。CustomStringConvertible を継承しています。
public protocol LosslessStringConvertible : CustomStringConvertible {
init?(_ description: String)
}
あいまいさなく、また、情報の欠落なく文字列として表現可能であることを表すプロトコルです。CustomStringConvertible の description で文字列化し、LosslessStringConvertible の init?(_: String) で文字列から値を生成するようにします。
String の init(_:) は LosslessStringConvertible に準拠した型の値を引数に取ります。
以下の例では、User を文字列と相互変換可能にしています。
struct User {
let id: String
let name: String
}
extension User: CustomStringConvertible {
var description: String {
return "\(id), \(name)"
}
}
extension User: LosslessStringConvertible {
init?(_ description: String) {
let v = description.components(separatedBy: ", ")
if v.count != 2 {
return nil
}
self = User(id: v[0], name: v[1])
}
}
print(User("123, taro") ?? "error")
let user = User(id: "456", name: "hanako")
print(String(user))
print(String(user))
TextOutputStreamable
以下のように定義されているプロトコルです。
public protocol TextOutputStreamable {
func write<Target>(to target: inout Target) where Target : TextOutputStream
}
このプロトコルに準拠すると TextOutputStream に対し値を write できるようになります。
以下の例では User の値を文字列に write し追加しています。
struct User {
let id: String
let name: String
}
extension User: TextOutputStreamable {
func write<Target>(to target: inout Target) where Target : TextOutputStream {
target.write("id: \(id), name: \(name)")
}
}
let user1 = User(id: "123", name: "taro")
let user2 = User(id: "456", name: "hanako")
var s = ""
user1.write(to: &s)
"\n".write(to: &s)
user2.write(to: &s)
print(s)
String Interpolation をカスタマイズ
StringInterpolationProtocol のページにカスタマイズ方法が記載されています。
-
\(x)
translates to appendInterpolation(x) -
\(x, y)
translates to appendInterpolation(x, y) -
\(foo: x)
translates to appendInterpolation(foo: x) -
\(x, foo: y)
translates to appendInterpolation(x, foo: y)
拡張は String.StringInterpolation
に対して行い、カスタマイズしたい型やパラメータを受け取る appendInterporation をオーバーロードしていきます。出力は appendLiteral を呼びます。
struct User {
let id: String
let name: String
enum NameFormat {
case uppercase
case lowercase
}
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ user: User, format: User.NameFormat) {
switch format {
case .lowercase:
self.appendLiteral(user.name.lowercased())
case .uppercase:
self.appendLiteral(user.name.uppercased())
}
}
}
let user = User(id: "123", name: "taro")
print("\(user, format: .uppercase)")