📚

【C#】SemVer2 - 高速なSemantic Versioning 2.0実装

2025/03/30に公開

SemVer2という、.NETでSemantic Versioningを扱うためのライブラリを公開しました。

https://github.com/nuskey8/SemVer2

基本的には.NET 6.0以降をターゲットにしていますが、.NET Standard 2.1にも対応しているので一応Unityでも使えます。また、System.Text.JsonやMessagePack-CSharp向けの拡張が用意されているほか、node-semverのようなCLIツールも用意してあります。

使い方

詳しい使い方はREADMEを読んでもらえれば良いのですが、大体の雰囲気はこんな感じ。

var semver = SemVer.Parse("1.0.0-alpha+001");

Console.WriteLine(semver);            // 1.0.0-alpha+001
Console.WriteLine(semver.Major);      // 1
Console.WriteLine(semver.Minor);      // 0
Console.WriteLine(semver.Patch);      // 0
Console.WriteLine(semver.Prerelease); // alpha
Console.WriteLine(semver.Build);      // 001

余計な機能を含めず、素直で使いやすいAPIになるように調整しています。また、比較演算子のオーバーロードも用意されているので直感的にバージョンの比較を行うことができます。

var a = SemVer.Parse("1.0.0");
var b = SemVer.Parse("1.0.1");
var c = SemVer.Parse("1.0.0-alpha");

Console.WriteLine(a < b); // True
Console.WriteLine(a < c); // False

パフォーマンス

ただSemantic Versioningを扱いたいだけならいくらでも選択肢はあるわけですが、SemVer2の特色はパフォーマンスです。構造体ベースなのでアロケーションも一切なく、かつ速度も他のどのライブラリよりも高速に動作するようにチューニングされています。

実装の工夫みたいなものは特になく、地道にSpan<T>経由で解析しているだけです。要するに他が遅すぎるだけ...

一応NuGet.Versioningは同一文字列に対するインスタンスを内部でキャッシュするようになっている(ベンチマークではキャッシュを使わないTryParseStrict()の方を利用している)のですが、余計なメモリ消費を増やすだけで大したメリットもないのでSemVer2では実装していません。そもそもこっちは構造体だし。

まあぶっちゃけパフォーマンスがそこまで重要かと言われると微妙なところですが、速いに越したことはないので良いでしょう。

バージョンを扱う

そもそもバージョン周りを扱うシチューション自体がまれなんですが、一応C#でも色々と選択肢はあります。

まずはSystem.Version。これは単にx.y.z形式のバージョンを扱うためのクラスで、Semantic VersioningのPrereleaseやBuild Metadataには対応していません。また、4番目のバージョンセグメント(Revision)に対応している点も異なります。一応「System.SemanticVersionを追加しよう」という提案が出されてはいるものの、実装が進んでる気配はなさそうです。

続いてNuGet.Versioning。これはNuGet内部で利用されている実装で、System.Versionとは異なりPrereleaseやBuild Metadataなどの情報も扱うことが可能です。(内部実装的にはSystem.Versionにこれらの情報を追加したものになっています)

ただし、NuGet独自の仕様が加わっているため厳密にはSemantic Versioningとは異なる点には注意。NugetVersionとSemantic Versioningとの違いは公式ドキュメントに記載されています。

https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#where-nugetversion-diverges-from-semantic-versioning

その他サードパーティのライブラリではベンチマークでも挙げたSemversemver.netあたりでしょうか。色々と機能は充実していそうですが、実装やAPIがやや古いのが気になるかなーという印象でした。(ベンチマークでのパフォーマンスもあまり良くなかったし)

とまあこんな感じで色々選択肢はありつつも、Semantic Versioningを扱うためだけに使うには微妙なものばかりという...

というわけでSemVer2はとにかくシンプルに、かつパフォーマンスに関しても十分になるように設計してあります。

文字列との相互変換

バージョンは文字列からパースして作成し、文字列としてどこかへ書き込むケースが多いでしょう。当然APIとしてはTryParse()/TryFormat()が必須です。またUTF-8で扱うことも考えると、ReadOnlySpan<char>だけでなくReadOnlySpan<byte>にも対応しておきたいところです。

SemVer2ではSemVer構造体がISpanFormattableISpanParsable<T>IUtf8SpanFormattableIUtf8SpanParsable<T>を実装しています。

public readonly struct SemVer : IEquatable<SemVer>, IComparable<SemVer>, IComparable
#if NET6_0_OR_GREATER
, ISpanFormattable
#endif
#if NET7_0_OR_GREATER
, ISpanParsable<SemVer>
#endif
#if NET8_0_OR_GREATER
, IUtf8SpanFormattable
, IUtf8SpanParsable<SemVer>
#endif
{
    ...
}

現代のC#の文字列操作はSpan<T>経由が基本です。stringを一切通さずに処理していきましょう。

また、System.Text.Json向けにSemVerJsonConverterが用意されているため、Jsonを通す際には通常の文字列として扱うことが可能です。

var json = JsonSerializer.Serialize(new Package("test", SemVer.Parse("1.0.0")));
Console.WriteLine(json); // {"Id":"test","Version":"1.0.0"}

record Package(string Id, SemVer Version);

Semantic Versioningの仕様

Semantic Versioningの仕様自体はsemver.orgで定められている通りなんですが、他のライブラリだと1.01.0.0.01.0aのような仕様に沿ってないバージョンもパースできるようになっていることが多いです。

SemVer2ではこのようなSemantic Versioning 2.0.0の仕様から外れた文字列はパースできないようになっています。この辺をどこまで許容するかは難しいところで、現状厳しすぎるのでもう少し制限を緩めてもいいかな、とは。

あんまりオレオレバージョニングは好きじゃないんだけどなあとは思いつつ、現実にはSemantic Versioningに沿ってないバージョニングいっぱいあるので...

まとめ

ふとした思い付きから何とまあ絶妙に使い所があるのかわからないライブラリが出来上がったわけなんですが、実装自体は悪くない仕上がりだと思ってます。パフォーマンスも十分だし、ほぼ構造体1つで完結しているシンプルさが逆に良い感じなんじゃないかと...!

というわけで使う場面があるかはわかりませんが、バージョンを扱う際には是非是非使ってみてください...!

Discussion