Renaming String.init<T>(_: T)
01 何が問題だったのか
Swift 2.2 の String には、引数ラベルなしの 1 引数イニシャライザが数多く用意されていました。Character、NSString、CharacterView、UnicodeScalarView から String を作るもの、数値から文字列を作るもの、UTF8View / UTF16View を受け取る失敗可能なものなどがこれにあたります。そして、それらとは別に、「任意の型の値をリフレクション経由でテキスト化する」汎用的なイニシャライザ init<T>(_: T) が同じく引数ラベルなしで定義されていました。
意図しない呼び出しが静かに通ってしまう
このリフレクション版イニシャライザは、受け取れる型が何でもよい(T が任意)ため、型違いで他のイニシャライザを呼んだつもりが、コンパイラにはこちらの呼び出しとして解決される、という事故が起きやすい設計でした。典型的には次の 2 パターンです。
- 本当は
CharacterやNSString用のような非失敗イニシャライザを呼びたかったが、引数の型を間違えている。 - 本当は
UTF8View/UTF16View用のような失敗可能イニシャライザを呼びたかったが、結果をString?ではなくStringの変数に代入してしまっている。
どちらの場合も、コンパイラは黙って init<T>(_: T) の呼び出しとして型付けしてしまい、警告も出ません。
let scalar: UnicodeScalar = "A"
let s = String(scalar)
// 期待: "A" という 1 文字の文字列
// 実際: リフレクション経由の "\u{0041}" のような表現になる恐れがあり、
// コンパイル時には区別できない
結果として、本人が気づかないうちに高コストなリフレクション機構が走ったり、値本来の情報を取りこぼした表現になったりします。そしてコンパイラからの手がかりがないため、発見自体がとても難しい、という問題がありました。
02 どのように解決されるのか
リフレクションに依存する String.init<T>(_: T) を、明示的なラベル付きの String.init<T>(describing: T) にリネームします。そして、「値を失わずに文字列化でき、同じ文字列から元の値を復元できる」型を表す新しいプロトコル LosslessStringConvertible を導入し、これに適合する型に対してだけラベルなしの String(x) という書き方を残します。Swift 3.0 で実装されています。
リフレクション版は describing: に移される
任意の型の値を「なんとか文字列で表したい」場合のイニシャライザは、describing: というラベル付きに変わります。ユーザコードから直接呼ばれることは多くなく、ログ出力など本当にリフレクションに頼りたい場面で使います。
struct Point { var x: Int; var y: Int }
let p = Point(x: 1, y: 2)
let s = String(describing: p)
// "Point(x: 1, y: 2)" のような表現
LosslessStringConvertible プロトコルの導入
新しく追加される LosslessStringConvertible は、CustomStringConvertible を継承するプロトコルです。description が値を失わない表現になっていることを約束し、その description を受け取って同じ値を復元できる失敗可能イニシャライザを要件に持ちます。
protocol LosslessStringConvertible : CustomStringConvertible {
/// 文字列表現から、適合型のインスタンスを生成する
init?(_ description: String)
}
たとえば整数値 1050 は "1050" という文字列として過不足なく表せ、同じ文字列から元の 1050 を復元できます。このような「値保存的な文字列化と復元」を持つ型が LosslessStringConvertible に適合します。
ラベルなしの String(x) は値保存的な型にだけ残す
String にはさらに次のイニシャライザが追加されます。
extension String {
init<T: LosslessStringConvertible>(_ v: T) { self = v.description }
}
これにより、LosslessStringConvertible に適合する型に対してはこれまで通りの String(x) 記法が使えます。一方、適合していない任意の型に対しては、ラベルなしの String(x) は成立せず、String(describing: x) と明示することになります。結果として、冒頭で挙げた「気づかないうちにリフレクション版が選ばれる」事故がコンパイル時に防がれます。
文字列補間の扱い
"\(x)" の形の文字列補間は、性能最適化として次のように実装を切り替えます。
- 補間対象の型がラベルなしの
Stringイニシャライザを持つ(LosslessStringConvertibleに適合しているなど)場合はそれを使う。 - 持たない場合は
String.init<T>(describing:)を使う。
利用側の書き方は従来どおりで、仕組み側だけが整理されます。
標準ライブラリで新プロトコルに適合する型
以下のプロトコル/型が LosslessStringConvertible に適合します。
プロトコル
FloatingPoint(IEEE 浮動小数点値を可逆な十進表現に変換するアルゴリズムを利用)Integer
型
Bool("true"/"false"が正準な表現となる)CharacterUnicodeScalarStringString.UTF8ViewString.UTF16ViewString.CharacterViewString.UnicodeScalarViewStaticString
今後の見通し
条件付き適合(conditional conformance)が利用できるようになった先の展望として、次のようなプロトコル/型を追加で LosslessStringConvertible に適合させることが考えられています。あくまで将来の候補で、本提案のスコープ外です。
RangeReplaceableCollection where Iterator.Element == Character/== UnicodeScalarSetAlgebra where Iterator.Element == Character/== UnicodeScalarRange/ClosedRange/CountableRange/CountableClosedRange(Bound: LosslessStringConvertibleの場合)
既存コードへの影響
リフレクション版 init<T>(_: T) を直接呼び出していたコードは、LosslessStringConvertible に適合していない型に対してはコンパイルエラーとなり、呼び出し側で String(describing:) への書き換えが必要になります。移行の副作用として、冒頭で述べた「意図せずリフレクション版が選ばれていた」既存バグが表面化することもあります。