UUID の version 対応とその他の機能拡張
UUID Version Support and Other Enhancements
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Foundation の UUID は、これまで version 4(ランダム)の UUID しか生成できませんでした。version 4 UUID は汎用識別子としては十分ですが、データベースの主キーには向きません。値が完全にランダムなため、B-tree インデックスへの挿入位置がキー空間全体に分散してしまい、キャッシュ局所性が悪化したり、書き込みのオーバーヘッドが増えたりするためです。
RFC 9562 ではこの問題を解決する version 7(時刻順序)UUID が定義されています。version 7 UUID は上位 48 ビットに Unix タイムスタンプ(ミリ秒)を埋め込むため、生成順に単調増加し、< で比較するだけで時系列順にソートできます。データベース主キーや分散システムでの識別子として広く使われるようになっていますが、Foundation でこれを生成するには UUID(uuid:) でバイト列を自分で組み立てる必要があり、RFC 9562 のビットレイアウトを理解しなければならない上にエラーが起きやすい状態でした。
加えて、現代の利用シーンに照らすと UUID には次のような不足もあります。
- UUID の version や variant を取得する API がない
- 既存の
uuidStringは大文字を返すが、Web API・データベース・URN 表記など多くの場面では小文字表記が慣例で、毎回lowercased()を呼ぶと中間Stringが割り当てられてしまう - 16 バイトの生バイト列に安全にアクセスする手段が
withUnsafeBytesやuuid_tのタプル要素を介する形に限られ、RawSpanを活用した境界チェック付きのアクセスができない - RFC 9562 で定義されている min UUID(全ビット 0)と max UUID(全ビット 1)をセンチネル値として扱う標準的なアクセサがない
02 どのように解決されるのか
UUID に version 7 UUID の生成手段と、version / variant の取得 API、RawSpan を使ったバイトアクセス、min/max などのセンチネル値、小文字表記、追加のイニシャライザを導入します。既存の UUID() は引き続き version 4(ランダム)UUID を生成し、挙動は変わりません。
// version 7 UUID を生成する
let id = UUID.version7()
// 任意の UUID の version を確認する
switch id.version {
case 7:
print("v7 UUID、生成時刻順にソート可能")
case 4:
print("v4 UUID")
default:
print("その他の version")
}
// 既存の UUID() は引き続き version 4 を生成する
let randomID = UUID()
assert(randomID.version == 4)
// センチネル値としての min / max UUID
let minID = UUID.min // 00000000-0000-0000-0000-000000000000
let maxID = UUID.max // FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
// コピーなしで生バイト列にアクセスする
let uuid = UUID()
let rawBytes: RawSpan = uuid.bytes // 16バイトの RawSpan
version 7 UUID の生成
version 7 UUID を生成するファクトリメソッドが追加されます。version 4 についても、明示的なファクトリメソッドが用意され、UUID() と等価です。
extension UUID {
public static func version4() -> UUID
public static func version7(
at date: Date? = nil,
offset: Duration = .zero
) -> UUID
public static func version7(
using generator: inout some RandomNumberGenerator,
at date: Date? = nil,
offset: Duration = .zero
) -> UUID
}
上位 48 ビットにはミリ秒精度の Unix タイムスタンプが入ります。続く 12 ビット(RFC 9562 でいう rand_a)にはサブミリ秒のタイムスタンプ精度を埋め込み(RFC 9562 6.2 節 Method 3)、残りの 62 ビット(rand_b)を乱数で埋めます。
date 引数を省略した場合、生成される UUID のタイムスタンプは 同一プロセス内で単調増加することが保証 されます。システムクロックが前回の呼び出しから進んでいない場合は、サブミリ秒側を 1 tick 進めることで順序を維持します。これは Go の google/uuid や PostgreSQL と同じアプローチで、高頻度生成や時刻の補正があってもソート順が崩れないようになっています。一方、date を明示的に指定した場合は単調増加の保証は適用されません。offset を指定すると、現在時刻(または指定された date)に対してその分だけ加算した時刻を埋め込めます。
version と variant の取得
任意の UUID から version と variant を取り出せるようになります。version は version ニブル(バイト 6 の上位 4 ビット、bit 48–51)から取得され、0〜15 の Int が返ります。setter は version ニブルだけを書き換え、他のビットには影響しません。
extension UUID {
public enum Variant: Sendable, Hashable {
case ncs // バイト 8 の variant ビットが 0xx
case rfc9562 // 10x
case microsoft // 110
case reserved // 111
}
public var version: Int { get set }
public var variant: Variant { get }
}
version 値は RFC 9562 で定義されており、たとえば 1(time-based)、3(name-based MD5)、4(random)、5(name-based SHA-1)、6(reordered time-based)、7(time-ordered)、8(custom)などがあります。Foundation が生成する UUID は variant .rfc9562(バイナリ 10)になります。
version 7 からの Date 取得
version 7 UUID では、埋め込まれたタイムスタンプを Date として取り出せます。version 7 以外では nil を返します。返される Date は RFC 9562 の定義どおりミリ秒精度です。サブミリ秒側に精度を追加で埋め込む実装があっても、この API では RFC 規定のバイトに格納されている分しか返されません。
extension UUID {
public var date: Date? { get }
}
min / max UUID
RFC 9562 5.9 節・5.10 節で定義された「全ビット 0」「全ビット 1」の特別な UUID にアクセスできます。「UUID がない」ことを表すセンチネルや、ソート範囲の境界として使えます。なお、min UUID と max UUID は version や variant フィールドに意味を持たず、version はそれぞれ 0 と 15 を返します。
extension UUID {
public static let min: UUID
public static let max: UUID
}
Swift では nil という名前の衝突を避けるため、RFC で「nil UUID」と呼ばれるものを Foundation では min と呼びます。
小文字の文字列表現
uuidString は大文字を返しますが、Web API、データベース、URN 表記(RFC 4122 §3)など多くの場面では小文字表記が慣例です。lowercasedUUIDString プロパティを使えば、uuidString.lowercased() のように中間 String を確保することなく小文字の表現が得られます。
extension UUID {
public var lowercasedUUIDString: String { get }
}
RawSpan を介したバイトアクセス
withUnsafeBytes や uuid_t のタプル要素を経由せず、境界チェック付きで 16 バイトの生バイト列にアクセスできます。bytes は読み取り専用、mutableBytes は直接書き換え可能です。どちらも UUID 値のライフタイムに依存します。
extension UUID {
public var bytes: RawSpan { get }
public var mutableBytes: MutableRawSpan { mutating get }
}
RawSpan / OutputRawSpan からのイニシャライザ
RawSpan から 16 バイトをコピーして UUID を作るイニシャライザと、OutputRawSpan を受け取るクロージャでバイト列を書き込んで UUID を作るイニシャライザが追加されます。後者は typed throws に対応し、uuid_t を経由せずに安全に UUID を組み立てられます。クロージャが 16 バイトちょうどを書き込まなかった場合(多すぎても少なすぎても)はトラップします。
extension UUID {
public init(copying bytes: RawSpan)
public init<E: Error>(
initializingWith initializer: (inout OutputRawSpan) throws(E) -> ()
) throws(E)
}
init(initializingWith:) の利用例は次のようになります。クロージャ内で生成する UUID が有効な形になるかは呼び出し側の責任です。
let uuid = UUID { output in
// 有効な UUID になるように 16 バイトを書き込む必要がある
output.append(...)
output.append(...)
}
03 今後の見通し
将来の発展として、次のような version の追加が挙げられています。
version 5(name-based SHA-1)
名前と名前空間から決定論的に UUID を生成する version 5 については、たとえば UUID.nameBased(name:namespace:) のようなファクトリメソッドを後続のProposalで追加することが考えられています。
version 8(custom)
アプリケーション固有のデータを埋め込める version 8 については、カスタムデータのビットを受け取り、version / variant フィールドを自動的に設定するイニシャライザとして公開することが考えられています。なお、本Proposalで追加される init(initializingWith:) を使えば、OutputRawSpan 経由で全バイトを直接設定して version 8 UUID を組み立てることも可能です。
これらはいずれも将来の構想であり、実現を約束するものではありません。