String.Indexの表示説明を改善する
Improving String.Index’s printed descriptions
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
String.Index は文字列の内部ストレージ上の位置を表す値で、UTF-8 または UTF-16 のコードユニット単位のオフセットを内部に保持しています(Swift の文字列は基本的に UTF-8 で格納されますが、Objective-C から橋渡しされた文字列は UTF-16 のままになっていることもあります)。
この String.Index を print などで文字列化すると、これまでは次のように不可解な表示になっていました。
let string = "👋🏼 Helló"
print(string.startIndex) // ⟹ Index(_rawBits: 15)
print(string.endIndex) // ⟹ Index(_rawBits: 983047)
print(string.utf16.index(after: string.startIndex)) // ⟹ Index(_rawBits: 16388)
print(string.firstRange(of: "ell")!) // ⟹ Index(_rawBits: 655623)..<Index(_rawBits: 852487)
これは String.Index がどの標準的な文字列変換プロトコルにも適合していなかったため、リフレクションに基づくデフォルトの変換経路が使われてしまっていた結果です。内部のビットパターンがそのまま露出するだけで、このインデックスが文字列のどの位置を指しているのかも、どのエンコーディングに基づくオフセットなのかも読み取れません。文字列処理のコードをデバッグする際、インデックスを print しても手がかりにならないというのは長年の不満点でした。
02 どのように解決されるのか
String.Index に CustomDebugStringConvertible への適合を追加し、debugDescription としてデバッグに役立つ形式の文字列を返すようにします。print や String(reflecting:) を通じて、そのインデックスが指している位置・想定しているエンコーディング・必要ならトランスコード後のオフセットまでを読み取れるようになります。
let string = "👋🏼 Helló"
print(string.startIndex) // ⟹ 0[any]
print(string.endIndex) // ⟹ 15[utf8]
print(string.utf16.index(after: string.startIndex)) // ⟹ 0[utf8]+1
print(string.firstRange(of: "ell")!) // ⟹ 10[utf8]..<13[utf8]
表示の読み方は次のとおりです。
15[utf8]は「UTF-8 エンコードされたStringのオフセット 15 のコードユニットを指す」ことを表します。startIndexのようにオフセットが 0 の位置はどのエンコーディングでも同じ位置になるため、0[any]と表示されます。- Swift 6.0 以降、一部プラットフォームでは
Stringが UTF-16 のままテキストを保持している場合があり、そのようなインデックスは[utf16]と表示されます。 0[utf8]+1の+1は、トランスコードした Unicode スカラ内でのオフセットです。上の例では先頭の絵文字 U+1F44B(UTF-8 ではF0 9F 91 8B、UTF-16 ではD83D DC4B)を UTF-16 として見たときの後続サロゲートDC4Bを指しており、この位置はストレージ上には存在せず、アクセスのたびに計算されます。
なお、debugDescription が返す文字列の具体的な書式や情報量は規定されていません。これは CustomDebugStringConvertible に適合する型でよく採られる方針で、String の内部実装が進化すればそれに合わせて表示も変わり得ます。書式をプログラムから解釈したり、debugDescription の内容に依存したコードを書いたりすることは想定されていません(ここで示した例も、現行実装を説明するための参考例にすぎません)。この書式は LLDB では Swift 5.8 から既にデータフォーマッタとして採用されており、そこで有用性が確かめられたものです。
バックデプロイと ABI について
この適合自体は Swift 6.1 の標準ライブラリで追加されたもので、ABI 安定プラットフォーム上ではバックデプロイできません。ただし String.Index.debugDescription プロパティ自体はバックデプロイ対応(@backDeployed(before: SwiftStdlib 6.1))になっており、古い OS 上でも明示的に debugDescription を呼び出せば新しい表示を得られます。
let str = "🐕 Doggo"
print(str.firstRange(of: "Dog")!)
// 旧 stdlib: Index(_rawBits: 327943)..<Index(_rawBits: 524551)
// 新 stdlib: 5[utf8]..<8[utf8]
print(str.endIndex.debugDescription)
// どの環境でも: 11[utf8]
もっとも、通常のコードで .debugDescription を明示的に呼び分けるスタイルは推奨されていません。基本的には「新しい stdlib では print の結果が読みやすくなる」という位置づけで理解しておけば十分です。
03 今後の見通し
debugDescription の情報を公開 API として取り出せるようにする
ここで debugDescription に表示している情報の一部、たとえばインデックスが想定するエンコーディングや UTF-8 / UTF-16 オフセットは、現状では公開 API から完全には取り出せません。debugDescription の書式は不安定なものなので、その文字列をパースしたり、内部のビットパターンを覗き見たりして取り出されるのは望ましくありません。そこで、次のような API を追加する案が示されています。
extension String {
@frozen enum StorageEncoding {
case utf8
case utf16
}
// ストレージのエンコーディング。対応するエンコーディングビューはランダムアクセス相当に振る舞う
var encoding: StorageEncoding { get }
}
extension String.Index {
// このインデックスを生んだ String のエンコーディング。不明なら nil
var encoding: String.StorageEncoding? { get }
// UTF-8 ストレージ上のオフセット。UTF-8 上で有効と分からない場合は nil
@available(SwiftStdlib 5.7, *)
var utf8Offset: Int? { get }
// UTF-16 ストレージ上のオフセット。UTF-16 上で有効と分からない場合は nil
@available(SwiftStdlib 5.7, *)
var utf16Offset: Int? { get }
}
String.Index は必ずしも自分のエンコーディングを知っているとは限らないため(ASCII 文字列のインデックスや startIndex はどちらのエンコーディングでも同じであり、Swift 5.7 より前のランタイムはインデックスのエンコーディングを追跡していませんでした)、encoding プロパティの戻り値はオプショナルです。utf8Offset / utf16Offset は SE-0241 で非推奨化された encodingOffset の機能を、より整理された形で再導入することにあたります。これらの API は微妙な設計課題を含むため、別の Proposal に切り出されることになっています。トランスコードされた位置のオフセットをどう露出するかも、その Proposal で扱うことが想定されています。
他の標準ライブラリ型へのデバッグ表示の追加
String.Index と同じように、見やすい表示があれば嬉しいのに現状ではリフレクション任せになっている標準ライブラリの型がいくつかあります。次のような型に CustomStringConvertible や CustomDebugStringConvertible を追加していく方向性が挙げられています。
Set.Index/Dictionary.IndexSlice/DefaultIndicesPartialRangeFrom/PartialRangeUpTo/PartialRangeThroughCollectionDifference/CollectionDifference.IndexFlattenSequence/FlattenSequence.IndexLazyPrefixWhileSequence/LazyPrefixWhileSequence.Index
いずれも将来の方向性として示されているもので、実現を約束するものではありません。