はじめに
C# で Guid を使うとき、いつも Guid.NewGuid() を呼ぶくらいで、他に何ができるのかあまり把握していませんでした。あるとき似たような疑問がいくつか出てきたので、まとめて調べてみることにしました。
-
Guid.Emptyとnew Guid()とdefault(Guid)は何が違うのか -
CompareToはどんなときに使えるのか -
NewGuid()は結局どのバージョンの UUID を作っているのか。.NET 10 で v7 に変わっていたりしないか
この記事は、その調べた内容のアウトプットです。
環境
- .NET 10.0
- C# 14.0
- VSCode
System.Guid とは
System.Guid は 128 ビット(16 バイト)の一意な識別子を表す構造体です。UUID とも呼ばれ、分散環境で衝突しない ID が欲しいときに使います。
まず、普段の使い方以外にどんなことができるのか、基本的な操作を整理しました。
生成する
一番よく使うのがランダム生成です。
var id = Guid.NewGuid();
空(全ゼロ)の Guid も作れます。
var empty = Guid.Empty; // 00000000-0000-0000-0000-000000000000
文字列にする
ToString() に書式指定子を渡すと出力形式を変えられます。
var id = Guid.NewGuid();
id.ToString(); // 既定("D"): ハイフン区切り 8-4-4-4-12
id.ToString("N"); // ハイフンなし 32 桁
id.ToString("B"); // {} で囲む
id.ToString("P"); // () で囲む
id.ToString("X"); // 0x プレフィックス付きの 16 進形式
普段は既定の ToString() しか使っていませんでしたが、ハイフンなしにしたいときに "N" が使えると知って便利だと思いました。
文字列からパースする
文字列から Guid に戻すこともできます。
var id = Guid.Parse(text); // 失敗時は例外
Guid.TryParse(text, out var id2); // 失敗時は false を返す(例外を投げない)
外部から受け取った文字列を扱うときは、例外を投げない TryParse のほうが扱いやすいと感じました。
バイト配列と相互変換する
byte[] bytes = id.ToByteArray(); // 16 バイトへ
var id2 = new Guid(bytes); // バイトから復元
Guid.Empty / new Guid() / default(Guid) の使い分け
3 つとも値としては同じ全ゼロの Guid になり、通常の利用上、結果に違いはありません。違うのは「読み手にどう伝わるか」だけです。
var a = Guid.Empty;
var b = new Guid();
var c = default(Guid);
// すべて 00000000-0000-0000-0000-000000000000
自分なりの使い分けはこう整理しました。
-
Guid.Emptyは「空の Guid と比較している/空を代入している」という意図が一番はっきり出る。if (id == Guid.Empty)のような比較で読みやすいと感じた -
defaultは「その型の既定値」を表す書き方で、ジェネリックのdefault(T)やフィールドの初期値など、型を問わず既定値を扱う文脈だと自然だと思った -
new Guid()は「新しい Guid を作っているように見えて空が返る」ので、個人的には積極的に選ぶ理由はないと感じた
「基本的に Guid.Empty」という認識で問題なさそうでした。
CompareTo はどんなときに使うのか
CompareTo は 2 つの Guid の大小を返すメソッドで、順序が必要な場面で使えます。
int cmp = id.CompareTo(other);
具体的には、Guid のリストを並べ替えるとき、SortedDictionary<Guid, T> や SortedSet<Guid> のキーにするときなどです。
ただ、Guid.NewGuid() で作った v4 はランダムなので、並べても生成順のような意味のある順序にはなりません。「毎回同じ結果になる決定的な順序」が欲しいだけの場面向けだと理解しました。生成時刻順に並べたい場合は、後述の v7 を使う話になります。
並び順で 1 つ気になったのは、.NET の Guid.CompareTo による並び順と、データベース(SQL Server の uniqueidentifier など)のソート順は一致するとは限らないことです。DB 側の並び順を前提にするなら、自分の環境で実際の並びを確認したほうがよさそうだと感じました。
NewGuid は .NET 10 で v7 になっていないか
結論から言うと、なっていませんでした。NewGuid() は .NET 10 でも v4(ランダム)を生成します。v7 を使いたい場合は、別のメソッド CreateVersion7() を明示的に呼ぶ必要があります。
公式ドキュメントでも、NewGuid() はバージョン 4 の UUID を生成すると記載されています。
もし「.NET 10 で NewGuid が v7 に変わった」という情報を見かけたら、それは誤りか、v7 を既定にする別ライブラリの話である可能性が高いと思います。標準の System.Guid では、v7 は CreateVersion7() 専用と切り分けておくのが正確でした。
.NET 9 で増えた機能
調べる過程で、v7 を含むいくつかの API が .NET 9 で追加されていたことを知りました。環境は .NET 10 ですが、追加自体は .NET 9 です。
時刻順にソートできる v7(CreateVersion7)
Guid.CreateVersion7() は、タイムスタンプを含む v7 UUID を生成します。そのため、v4 と異なり時系列に沿った自然なソート順を持ちます。
var v7 = Guid.CreateVersion7();
var v7t = Guid.CreateVersion7(DateTimeOffset.UtcNow); // 時刻を明示
引数に DateTimeOffset を取るので、TimeProvider.GetUtcNow() を渡せばテストで時刻を差し替えられます。
なお、v7 をデータベースのキーに使うときの挙動については自分では検証していないので、実際に使うなら別途確認したいと思っています。
バージョンを確認できる(Version)
生成された Guid が何バージョンかを Version プロパティで確認できます。
Guid.NewGuid().Version; // 4
Guid.CreateVersion7().Version; // 7
今回、NewGuid が本当に v4 なのかを確かめるのにそのまま使えました。Variant プロパティと AllBitsSet(全ビットが 1 の Guid)も同じ .NET 9 で追加されています。
まとめ
-
Guid.Empty/new Guid()/default(Guid)は結果が同じ。読みやすさで選ぶなら、比較・空判定はGuid.Empty、既定値の文脈はdefaultが自然だと感じた -
CompareToは並べ替えやSortedSet/SortedDictionaryのキーなど、順序が必要なときに使う。v4 はランダムなので順序に意味はない -
NewGuid()は .NET 10 でも v4 のまま。v7 が欲しいときはCreateVersion7()を明示的に呼ぶ -
CreateVersion7/Version/Variant/AllBitsSetは .NET 9 で追加された
調べる前は「Guid = NewGuid() で作るもの」くらいの認識でしたが、用途に応じて生成方法や比較の使い分けがあることがわかりました。
参考になったら いいね や ストック をお願いします!
同じような疑問を持ったことがある方のコメントもお待ちしています。
参考
- Guid 構造体 - .NET(Microsoft Learn)
- Guid.NewGuid メソッド - .NET(Microsoft Learn)
- Guid.CreateVersion7 メソッド - .NET(Microsoft Learn)
関連リンク
技術ブログでも学びや検証内容をまとめています。