Span を使った Data の操作
Data Manipulation with Spans
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Data は untyped なメモリの連続領域を表すコンテナ型です。Data は RangeReplaceableCollection に適合しているため、Sequence<UInt8> を介してバイト列を操作する多くの API を継承しています。
一方で、ライブラリの世界では性能特性とライフタイム保証を目的として、連続メモリを表すために Span や RawSpan といったライフタイムに束縛された span 型を提供する API が増え始めています。span を受け渡しする API が広がるにつれ、Data からもそれらと直接やり取りできることが重要になってきました。Data はすでに初期化済みの領域へアクセスするための bytes プロパティを備えていますが、span を通じて Data を初期化したり書き換えたりするための API は用意されていませんでした。
さらに、これまでに承認されていた span ベースの初期化・追加 API は OutputSpan<UInt8> を使い、命名も個別に決められた状態でした。しかし SE-0527 が承認され、UniqueArray のような span ベースで RangeReplaceableCollection に相当する API の命名規約が固まったため、Data の span ベース API もこの規約に合わせて整理し直す必要が出てきました。
02 どのように解決されるのか
UniqueArray(SE-0527)で確立された命名規約に沿って、Data に span ベースの初期化・変更 API を追加します。ただし要素型を持つ Span<Element> ではなく RawSpan を使います。これは、Data が「untyped なメモリの連続領域」という意味づけを持つためで、RawSpan の方がその性質によく合うからです。要素型付きの Span<T> としてバイト列を扱いたい場合は、SE-0525 の API を使って RawSpan と Span<T>、OutputRawSpan と OutputSpan<T> を相互に変換できます。
追加される API
RawSpan から Data を作ったり、末尾へ追加したりする操作は、次のように書けます。
var someBytes: RawSpan = /* span を提供する API からバイト列を受け取る */
var data = Data(copying: someBytes)
data.append(copying: someBytes)
Data の内部ストレージを直接編集したい場合は、OutputRawSpan を受け取るクロージャを使います。edit(_:) はクロージャを最大1回だけ呼び出し、その中でバイトの追加・削除・並べ替えを自由に行えます(ただし span 自体の差し替えや capacity の変更はできません)。クロージャが終了すると、Data は OutputRawSpan の最終的な内容に更新されます。
data.edit { outputRawSpan in
// outputRawSpan を通じて Data の中身を直接書き換える
}
指定位置へバイトを挿入する API では、挿入するバイト数と位置を渡し、クロージャで未初期化のストレージを直接埋められます。
var prefix: RawSpan = /* バイト列 11, 99 を持つ RawSpan */
var buffer = Data(capacity: 20, copying: prefix)
var i: UInt8 = 0
buffer.insert(addingCount: 3, at: 1) { target in
while !target.isFull {
target.append(i)
i += 1
}
}
// buffer は 11, 0, 1, 2, 99 というバイト列になる
RawSpan をそのままコピーして挿入する insert(copying:at:) や、範囲を置き換える replaceSubrange(_:addingCount:initializingWith:) / replaceSubrange(_:copying:) も同じ考え方で用意されます。クロージャを使う挿入・置換では、クロージャが OutputRawSpan を最後まで埋めなかったり途中でエラーを throw したりした場合でも、それまでに初期化できたバイトは保持されます。これらの API は要素型を持つコレクションの操作と対応する initializer を使い、throws(E) の typed throws にも対応しています。
追加される主な API は次のとおりです。
extension Data {
public init(capacity: Int? = nil, copying span: RawSpan)
public mutating func edit<E: Error, R: ~Copyable>(
_ body: (inout OutputRawSpan) throws(E) -> R
) throws(E) -> R
public mutating func append(copying newBytes: RawSpan)
public mutating func insert<E: Error>(
addingCount newBytesCount: Int,
at index: Int,
initializingWith initializer: (inout OutputRawSpan) throws(E) -> Void
) throws(E)
public mutating func insert(copying newBytes: RawSpan, at index: Int)
public mutating func replaceSubrange<E: Error>(
_ subrange: Range<Int>,
addingCount newBytesCount: Int,
initializingWith initializer: (inout OutputRawSpan) throws(E) -> Void
) throws(E) -> Void
public mutating func replaceSubrange(_ subrange: Range<Int>, copying newBytes: RawSpan)
}
既存 API の修正
これまでに承認されていた span ベースの初期化・追加 API も、RawSpan / OutputRawSpan に揃える形で整理します。
Data(rawCapacity:initializingWith:)とData.append(addingRawCapacity:initializingWith:)は削除します。Data(capacity:initializingWith:)とData.append(addingCapacity:initializingWith:)は、クロージャの引数をOutputSpan<UInt8>からOutputRawSpanに変更します。あわせて後者は SE-0527 の命名規約に従いData.append(addingCount:initializingWith:)に改名します。
extension Data {
public init<E: Error>(
capacity: Int,
initializingWith initializer: (inout OutputRawSpan) throws(E) -> Void
) throws(E)
public mutating func append<E: Error>(
addingCount newByteCount: Int,
initializingWith initializer: (inout OutputRawSpan) throws(E) -> Void
) throws(E)
}
これにより、Data の span ベース API は raw な span 型に一本化されます。untyped 版と UInt8 版を二重に用意する必要がなくなり、Data が「バイト列を untyped なメモリとして扱う」という約束を素直に表現できます。これらの修正対象 API は承認済みですが正式にはまだ出荷されていないため、安定した利用者に影響を与えずに変更できます。
なお、これらの API のavailabilityは span 型のavailabilityに揃えられ、span 型が backdeploy されている範囲まで遡って利用できます。