Swift Digest
SE-0163 | Swift Evolution

String Revision: Collection Conformance, C Interop, Transcoding

Proposal
SE-0163
Authors
Ben Cohen, Dave Abrahams
Review Manager
John McCall
Status
Implemented (Swift 4.0)

01 何が問題だったのか

Swift 4 String Manifesto に基づく一連の改善のうち、String 型のコレクション適合・C 文字列相互運用 API・トランスコーディング基盤を扱うProposalです。Swift 3 までの String には、長年のあいだ以下のような課題がありました。

StringCollection ではなかった

Swift 2 で String から Collection 適合が外されていました。その結果、for c in string で文字を順に取り出したり、map / filter などのコレクション向けアルゴリズムをそのまま String に適用したりすることができず、string.characters を経由する必要がありました。

連結時に一部の文字(grapheme cluster)が結合することで RangeReplaceableCollection のセマンティクスと微妙に食い違うのが主な理由でしたが、現実的な影響は小さく、直接コレクションとして扱えないことによる不便さの方が大きいと再評価されました。

スライスが String 自身でメモリリークの温床になりやすい

String は自分自身を SubSequence として返していました。そのため、大きな String からごく一部だけスライスして長期間保持すると、ArraySlice と同じ仕組みで元の String のストレージ全体が保持され続け、見かけ上のメモリリークを招きます。利用者からはスライスが String なのか独立したコピーなのか区別できず、事故が起こりやすい状態でした。

C 文字列との相互運用 API が散在していた

nul 終端の C 文字列と String を相互変換する API が複数あり、String から C 文字列を作る方法が 6 通り、逆方向が 4 通りと乱立していました。エンコーディング不正時の扱い(修復するか失敗するか)も API ごとに差があり、どれを使えばよいか分かりにくい状態でした。

トランスコーディング基盤の使いづらさ

既存の UnicodeCodec プロトコルは、エンコーディング間の変換を直接行えず、いったん Unicode スカラー値に落としてから再エンコードする必要がありました。また、逆方向のデコードをサポートしていないため、String の各種ビュー(UTF-8 ビューなど)を双方向コレクションとして扱うのが難しく、パフォーマンスの面でも改善の余地がありました。Latin-1 用のコーデックが標準ライブラリに存在しないという穴もありました。

02 どのように解決されるのか

String を再びコレクションとして扱えるようにし、スライス用の独立した型 Substring と、両者を抽象化する StringProtocol を導入します。あわせて C 文字列相互運用 API を整理し、Unicode まわりの低レイヤを Unicode 名前空間に集約した新しいトランスコーディング基盤に置き換えます。

String が再び Collection になる

StringBidirectionalCollectionRangeReplaceableCollection に適合し、Character のコレクションとして直接扱えるようになります。

let greeting = "Hello, Swift!"
for c in greeting {
    print(c)
}

let uppercased = greeting.map { $0.uppercased() }
let vowels = greeting.filter { "aeiouAEIOU".contains($0) }

これにより string.characters を経由する必要がなくなり、一般的なコレクションアルゴリズムをそのまま利用できます。

Substring 型の導入

String のスライスは、String 自身ではなく新しい Substring 型として返されるようになります。SubstringString とほぼ同じ API を持ち、StringProtocol にも適合するため、多くの処理はどちらに対しても同じように書けます。

let line = "name=Swift"
let equalsIndex = line.firstIndex(of: "=")!
let key: Substring   = line[..<equalsIndex]   // "name"
let value: Substring = line[line.index(after: equalsIndex)...] // "Swift"

Substring は元の String のストレージを共有するため、生成は軽量です。ただし ArraySlice と同様、長期保存はメモリリークの温床となるため、長く保持する際には String に戻します。

// 長期保存する場合は String に明示変換
let savedKey = String(key)

既存の extension String { ... } を両方に適用したい場合は extension StringProtocol { ... } に書き換えます。そのうえで、具体 String を要求する API に渡すときだけ String(self) でコピーを作ります。SelfString のときはほぼノーコスト、Substring のときは明示的なコピーとなり、前述のメモリリークを防げます。

なお、Swift 3 互換モードではスライス結果が従来通り String を返す非推奨 API が残るため、既存コードは段階的に移行できます(Swift 4 では Substring 返却が既定になります)。

整理された C 文字列相互運用 API

String の C 文字列関連 API は、UTF-8 用と任意エンコーディング用の各 2 本ずつに整理されます。

extension String {
    // UTF-8 / 任意エンコーディングの nul 終端列から String を作る
    init(cString nulTerminatedUTF8: UnsafePointer<CChar>)
    init<Encoding: Unicode.Encoding>(
        decodingCString nulTerminatedCodeUnits: UnsafePointer<Encoding.CodeUnit>,
        as: Encoding.Type
    )

    // 任意エンコーディングのコードユニット列(非 nul 終端でも可)から String を作る
    init<C: Collection, Encoding: Unicode.Encoding>(
        decoding codeUnits: C, as encoding: Encoding.Type
    ) where C.Iterator.Element == Encoding.CodeUnit

    // String の中身を nul 終端の C 文字列として参照する
    func withCString<Result>(
        _ body: (UnsafePointer<CChar>) throws -> Result
    ) rethrows -> Result
    func withCString<Result, Encoding: Unicode.Encoding>(
        encodedAs: Encoding.Type,
        _ body: (UnsafePointer<Encoding.CodeUnit>) throws -> Result
    ) rethrows -> Result
}

これらの初期化子では、不正なエンコーディング列を検出した場合に最長の有効プレフィックスを U+FFFD(Unicode 置換文字)に置き換える修復を行います。検出時に失敗させたい場合は、エンコーディング側の API を使います。非修復版の旧 API は Swift 4 で非推奨となり、将来的に削除されます。String をそのまま UnsafePointer<CChar> を受け取る関数に渡せる挙動は従来通り利用できます。

Unicode 名前空間と新しいエンコーディング・プロトコル

低レベルな Unicode 処理は Unicode 名前空間(ケースレス enum)に集約されます。既存のトップレベル名は後方互換のためのエイリアスとして残ります。

enum Unicode {
    enum ASCII  : Unicode.Encoding { /* ... */ }
    enum UTF8   : Unicode.Encoding { /* ... */ }
    enum UTF16  : Unicode.Encoding { /* ... */ }
    enum UTF32  : Unicode.Encoding { /* ... */ }
    enum Latin1 : Unicode.Encoding { /* ... */ } // 新設
    struct Scalar { /* ... */ }
    typealias Encoding = _UnicodeEncoding
    typealias Parser   = _UnicodeParser
    enum ParseResult<T> {
        case valid(T)
        case emptyInput
        case error(length: Int)
    }
}

// 旧称はエイリアスとして存続(Swift 3 互換)
// UTF8, UTF16, UTF32, UnicodeScalar など

UnicodeCodec を置き換える Unicode.Encoding プロトコルは、エンコーディング間の直接トランスコードと、前方・後方の両方向のパースを表現できます。

  • decode / encode / transcode により、スカラー値を経由せずエンコーディング間で直接変換できます。
  • 関連型の ForwardParser / ReverseParser によって、コードユニット列を前向き・後ろ向きの両方向にスキャンしてスカラー単位に切り出せます。

これにより String の各種エンコーディングビューを双方向コレクションとして扱えるようになり、性能面での改善も期待できます。また、標準ライブラリに欠けていた Latin1 コーデックが追加されます。既存の UnicodeCodecUnicode.Encoding を継承する形にリファイン(refinement)され、Swift 4 で非推奨となります。

Future Directions

Proposal の範囲は低レイヤの API に限定されており、今後は Unicode 名前空間の上に、任意のストレージに対するトランスコードやセグメント化を行う汎用的な Iterator / Sequence / Collection ビューを追加していく見通しが示されています(別 Proposal として提案予定)。いずれも将来の方向性の提示であり、実現を約束するものではありません。