String Gaps and Missing APIs
01 何が問題だったのか
Swift 5 で String の内部表現が UTF-8 に統一されて以降、String やその周辺の型には、内部的には存在するのに公開されていない「あれば便利なのに」という小さな API が散在していました。たとえば次のようなものです。
- ある UTF-8 / UTF-16 / UTF-32 のコードユニットが ASCII スカラーを表しているかどうかを判定する手段
- ある
Unicode.Scalarを UTF-8 にエンコードしたときに何バイト必要かを返す関数 - UTF-16 のコードユニットがサロゲートかどうかを判定する関数
String.IndexをString以外のStringProtocol適合型の間で受け渡すためのジェネリックなイニシャライザSubstringから元のStringを取り出すためのアクセサ(Slice.base相当)CharacterやUnicode.Scalarの UTF-8 / UTF-16 コードユニットへのアクセス
これらが揃っていないと、Swift-NIO のような高性能が求められるライブラリでも、Character から UTF-8 バイト列を取り出すために同じような定義を繰り返し書かざるを得ませんでした。また、Unicode.Scalar には UTF-16 コードユニットを RandomAccessCollection として見る UTF16View があるのに UTF-8 側にそれがなく、対称性も欠けていました。
02 どのように解決されるのか
Swift 5.1 で、String 周辺に不足していた API をまとめて追加します。大きく分けて次の5グループです。
Unicode エンコーディングへの小さな追加
Unicode.ASCII / UTF8 / UTF16 / UTF32 に、コードユニットが ASCII スカラーを表すかを判定する isASCII(_:) が追加されます。さらに Unicode.UTF8 には、Unicode.Scalar を UTF-8 にエンコードするのに必要なコードユニット数(1〜4)を返す width(_:)、Unicode.UTF16 には高位・低位サロゲートかを判定する isSurrogate(_:) が加わります。
let anApple: Unicode.Scalar = "🍎"
print(UTF8.width(anApple)) // 4
print(Unicode.UTF8.isASCII(0x41)) // true
print(Unicode.UTF16.isSurrogate(0xD800)) // true
String.Index と Range<String.Index> のジェネリックイニシャライザ
これまで String に対する具象版しかなかったイニシャライザが、StringProtocol 上でジェネリックに使えるようになります。Substring 上のインデックスを扱うようなコードでも、いちいち String に変換せずに済みます。
extension String.Index {
public init?<S: StringProtocol>(
_ sourcePosition: String.Index, within target: S
)
}
extension Range where Bound == String.Index {
public init?<S: StringProtocol>(_ range: NSRange, in string: __shared S)
}
たとえば、unicodeScalars ビュー上のインデックスが文字境界に一致していれば、対応する String.Index を得られます。
let cafe = "Cafe\u{0301}"
let scalarsIndex = cafe.unicodeScalars.firstIndex(of: "e")!
let stringIndex = String.Index(scalarsIndex, within: cafe)!
print(cafe[...stringIndex]) // "Café"
Substring.base
Slice が持っている base と同じく、Substring からも元の String を取り出せるようになります。
extension Substring {
public var base: String { get }
}
Character の UTF-8 / UTF-16 ビュー
これまで内部的には存在していた Character.UTF8View / Character.UTF16View が公開され、Character から直接 UTF-8 / UTF-16 のコードユニットにアクセスできるようになります。型エイリアスとして String.UTF8View / String.UTF16View と同じ型になっているため、ビューの使い勝手はそのまま揃っています。
let c: Character = "🍎"
for byte in c.utf8 {
print(byte)
}
Unicode.Scalar.UTF8View
既存の Unicode.Scalar.UTF16View に対応するかたちで Unicode.Scalar.UTF8View が追加され、RandomAccessCollection として扱えるようになります。インデックス型は Range<Int> で、startIndex / endIndex / subscript を通じて UTF-8 コードユニットに直接アクセスできます。
let apple: Unicode.Scalar = "🍎"
let utf8 = apple.utf8
print(utf8.count) // 4
print(Array(utf8)) // [240, 159, 141, 142]
これで Unicode.Scalar 単体から UTF-8 バイト列をランダムアクセス可能なコレクションとして取り出せるようになり、UTF-16 側との対称性が取れます。