Expose code unit initializers on String
01 何が問題だったのか
文字列とバイト列を相互変換する処理は、シリアライズ、バイナリ・テキストの各種フォーマット、ネットワーク通信、暗号処理など、さまざまな場面で必要になります。しかし Swift 2.x 当時、外部から取り込んだコードユニット列(UTF-8 / UTF-16 など)を String に変換する公開 API は String.fromCString(_:) と String.fromCStringRepairingIllFormedUTF8(_:) に限られており、使い勝手と性能の両面で不自由がありました。本提案は、任意のエンコーディングのコードユニット列から直接 String を作る初期化 API を公開しようとしたものでした。
fromCString しか速い選択肢がなかった
標準ライブラリには UnicodeCodec プロトコル(当時の UnicodeCodecType)や String.UnicodeScalarView といった「Swift らしい」API が用意されていたため、素朴にはそれらを組み合わせてコードユニット列から String を組み立てられそうに見えます。ところが実測すると、fromCString(_:) が圧倒的に高速で、他のアプローチは性能面で大きく劣るという状況でした。
内部的には String に _fromCodeUnitSequence(_:input:) という、バッファコピーによって効率的かつ安全に String を初期化する機能が備わっており、fromCString はほぼ唯一その恩恵を受けている公開 API でした。言い換えれば、パフォーマンスの出る経路がC文字列向けに限定されてしまっており、Unicode 的に安全であることは保証されないまま、他の経路だけが遅い、という不均衡が生じていたのです。
fromCString の制約
fromCString(_:) は UTF-8 固定で、終端に NUL を要求します。そのため、次のようなケースで扱いが面倒になります。
- 長さが先頭で与えられるフォーマット(多くのバイナリフォーマット)や、別の区切り文字で終端される非構造的なペイロードを扱う場合、元バッファに NUL を付けるために一度コピーするか、遅い「1文字ずつの append」経路に戻るしかない
- 文字列自体に NUL を含めたい場合は扱えない
strlenで長さを測るコストが必ず発生する。呼び出し側で長さが分かっていても短絡できない
任意のコードユニット列から String を作る語彙が無かった
結局のところ、「UTF-8 でも UTF-16 でも、任意のエンコーディングのコードユニット列(UnsafeBufferPointer や配列、スライスなど)から、最小コピーで String を組み立てる」という、系統立った公開 API が無かった、というのがこの提案の出発点でした。
02 どのように解決されるのか
この提案は Rejected(却下) となりました。提案されていた形の API が String に追加されることはありませんでしたが、ここで指摘された課題意識(任意のエンコーディングのコードユニット列から効率よく String を作りたい、fromCString 系を整理したい)は、その後の標準ライブラリの改修の中で別の形で解消されています。
提案されていた内容(却下されたもの)
中心となる API は、内部に存在していた String._fromCodeUnitSequence(_:input:) を一般化して公開するもので、任意の UnicodeCodec(当時は UnicodeCodecType)と任意のコレクションを受け取る静的メソッドとして提案されていました。
static func decode<
Encoding: UnicodeCodecType,
Input: CollectionType
where Input.Generator.Element == Encoding.CodeUnit
>(
_: Input,
as: Encoding.Type,
repairingInvalidCodeUnits: Bool = default
) -> (result: String, repairsMade: Bool)?
よく使うケース向けに、repairingInvalidCodeUnits の真偽をメソッド名に畳み込んだ String の初期化子も用意される想定でした。
init<...>(codeUnits: Input, as: Encoding.Type)
init?<...>(validatingCodeUnits: Input, as: Encoding.Type)
さらに、fromCString(_:) / fromCStringRepairingIllFormedUTF8(_:) と直接対応するポインタ版の初期化子も提案されていました。
init(cString: UnsafePointer<CChar>)
init?(validatingCString: UnsafePointer<CChar>)
ここで注目すべきは、cString: と validatingCString: でデフォルトの挙動が入れ替わっている点です。旧 fromCString(_:) は不正なコードユニット列に遭遇すると失敗していましたが、提案された init(cString:) は無条件に成功し、検証が必要な場合は init?(validatingCString:) を明示的に選ぶ、という役割分担になっていました。
採択されていれば、旧 fromCString 系は新しい初期化子へ誘導する deprecation を経て Swift 3 で置き換わる見込みでした。
却下の背景
Swift Evolution 初期の提案群を整理する過程で、この提案は単独採択には至らず Rejected として処理されました。課題自体は妥当ですが、よりきれいな解決策を別途検討すべき、という判断です。提案の本文中でも「長期的には String.UTF8View / String.UTF16View をミュータブルにして append(_:) / appendContentsOf(_:) を O(1) で動かせるようにする方向の方が、API 設計としては筋が良いかもしれない」と触れられており、実際その後の String の再設計はそうした方向に進みました。
現在のSwiftでの代替
本提案の目的は、Swift 3 以降で段階的に導入された以下の公開 API によって、おおむね達成されています。
C 文字列から String を作る方は、String 本体の初期化子として次のように整理されました。提案されていた通り、デフォルトは成功・検証版を別に用意する形です。
// UTF-8 のヌル終端C文字列から String を作る(不正なコードユニットは置換される)
let s1 = String(cString: pointer)
// 検証つき。不正なコードユニットがあれば nil を返す
let s2 = String(validatingCString: pointer)
任意のコードユニット列(長さが分かっているバッファやコレクション)から String を作る場合は、String の init(decoding:as:) と init?(validating:as:) を使います。エンコーディングには UTF8 / UTF16 / UTF32 のいずれかを渡します。
let utf8: [UInt8] = [0x48, 0x65, 0x6C, 0x6C, 0x6F] // "Hello"
// 不正なコードユニットは U+FFFD に置換されつつ成功する
let s3 = String(decoding: utf8, as: UTF8.self)
// 検証つき。不正な並びがあれば nil を返す
if let s4 = String(validating: utf8, as: UTF8.self) {
print(s4)
}
バッファ(UnsafeBufferPointer など)や Array、ArraySlice など、任意のコレクションをそのまま渡せるため、提案が目指していた「最小コピーで外部のコードユニット列を取り込む」用途はこちらで書けるようになっています。
提案時点で議論されていた「String.UTF8View / String.UTF16View をミュータブルにする」方向についても、以降の String の再実装でビュー側の書き換え API が整備され、コードユニット列を組み立てながら String を構築するユースケースもカバーされています。_fromCodeUnitSequence(_:input:) の直接公開という形ではありませんが、読者が実務で必要とする「バイト列と String の相互変換を高速に行いたい」という要求は、現在の String API で十分に応えられる状態になっています。