Versionクラスとは?

Versionはバージョン番号を表現するためのC#標準のクラスです。

System名前空間にあるので、追加のパッケージは必要ありません。

バージョンをMajor.Minor.Build.Revisionの4つの数値として保持し、 大小比較や文字列からの変換を用意してくれています。

なぜ文字列のまま比較してはいけないのか

バージョン番号は"1.9.0"のような文字列で扱うことが多いですが、 文字列のまま比較すると意図しない結果になります。

// 文字列としての比較。負の値が返る = 1.10.0の方が小さいと判定される
Debug.Log("1.10.0".CompareTo("1.9.0")); // -1

文字列の比較は先頭から1文字ずつ見ていくため、 1と9を比べた時点で"1.10.0"の方が小さいと判断されてしまいます。 バージョン1.9.0から1.10.0に上げた瞬間、 アップデート判定が動かなくなるという、なかなか気付きにくい不具合です。

Versionに変換してから比較すれば、この問題は起きません。

// 数値として比較されるので期待通りの結果になる
Debug.Log(new Version(1, 10, 0) > new Version(1, 9, 0)); // True

バージョンを比較するなら、文字列のままにせずVersion に変換することで安全に比較します。

Versionクラスの基本的な使い方

インスタンスを生成する

コンストラクタに数値を渡して生成します。 引数は2つから4つまで指定できます。

using System;

var version1 = new Version(1, 0);          // 1.0
var version2 = new Version(1, 2, 3);       // 1.2.3
var version3 = new Version(1, 2, 3, 4);    // 1.2.3.4

文字列から変換する

文字列から生成する場合はVersion.Parseを使います。

var version = Version.Parse("1.2.3");

ただしParseは変換に失敗すると例外を投げます。 サーバーから受け取った値のように、 何が入ってくるか保証できない文字列を扱うときはVersion.TryParseを使ってください。

if (Version.TryParse("1.2.3", out var version))
{
    Debug.Log($"変換成功: {version}");
}
else
{
    Debug.Log("変換に失敗しました");
}

TryParseは失敗しても例外を投げず、戻り値のboolで成否を返します。 外部から来た値を扱う場面では、基本的にこちらを選んでおくと安全です。

各要素を取得する

生成したVersionの各要素は、プロパティで個別に取り出せます。

var version = new Version(2, 5, 1);

Debug.Log(version.Major);      // 2
Debug.Log(version.Minor);      // 5
Debug.Log(version.Build);      // 1
Debug.Log(version.Revision);   // -1

ここで注意したいのが、指定しなかった要素は0ではなく-1になる点です。

上の例ではRevisionを指定していないので-1が返ってきています。

「未指定」と「0が指定された」を区別するための仕様で、

この違いが次の比較にも効いてきます。

バージョンを比較する

比較演算子で比較する

Versionは比較演算子に対応しているので、そのまま大小を比べられます。

var current = Version.Parse("1.9.0");
var latest = Version.Parse("1.10.0");

if (current < latest)
{
    Debug.Log("新しいバージョンがあります");
}

<、>、<=、>=、==、!=のいずれも使えます。 Majorから順に数値として比較され、差がついた時点で結果が決まります。

CompareToで比較する

大小関係を数値として受け取りたい場合はCompareToを使います。

var result = Version.Parse("1.9.0").CompareTo(Version.Parse("1.10.0"));

// 負の値: 自分の方が古い / 0: 同じ / 正の値: 自分の方が新しい
Debug.Log(result); // -1

戻り値の符号だけを見る使い方が基本です。 具体的な数値そのものに意味はないので、-1や1という値に依存した書き方は避けてください。

1.0と1.0.0は等しくない

先ほどの-1の仕様がここで効いてきます。 要素数が違うバージョン同士は、たとえ見た目が同じでも等しくなりません。

var a = new Version(1, 0);      // Build = -1
var b = new Version(1, 0, 0);   // Build = 0

Debug.Log(a == b);  // False
Debug.Log(a < b);   // True

aのBuildは-1、bのBuildは0なので、-1 < 0でaの方が古いと判定されます。 サーバー側が"1.0"、アプリ側が"1.0.0"という表記の揺れを持っていると、 同じバージョンのつもりが「アプリの方が古い」と判定されてしまいます。

比較する両者のバージョン表記は、桁数を揃えておきましょう。

Unityでの実践 - 強制アップデート判定

ここまでの内容を、実際によく使う強制アップデート判定に落とし込んでみます。

アプリのバージョンを取得する

実行中のアプリのバージョンはApplication.versionで取得できます。 これはPlayer SettingsのVersionに入力した文字列がそのまま返ってきます。

Debug.Log(Application.version); // Player Settingsに設定した文字列

最低要求バージョンと比較する

サーバーやリモート設定から「動作に必要な最低バージョン」を配信し、 アプリ側のバージョンと比較します。

using System;
using UnityEngine;

public class VersionChecker : MonoBehaviour
{
    // 本来はサーバーやRemote Configから取得する値
    [SerializeField] private string _requiredVersion = "1.2.0";

    private void Start()
    {
        if (NeedsUpdate(Application.version, _requiredVersion))
        {
            Debug.Log("アップデートが必要です");
        }
    }

    private bool NeedsUpdate(string current, string required)
    {
        // 現在のバージョンが読めない場合はアップデート不要として扱う
        if (!Version.TryParse(current, out var currentVersion))
        {
            Debug.LogWarning($"現在のバージョンを解析できませんでした: {current}");
            return false;
        }

        // 要求バージョンが読めない場合も同様に通す
        if (!Version.TryParse(required, out var requiredVersion))
        {
            Debug.LogWarning($"要求バージョンを解析できませんでした: {required}");
            return false;
        }

        return currentVersion < requiredVersion;
    }
}

パースに失敗したときにfalseを返しているのがポイントです。

配信する値のタイプミス1つでアプリが起動できなくなるより、 判定をすり抜けてもらった方が被害は小さくなります。

アップデートが必要と判定された後は、 ストアへ誘導する導線を用意することになります。

外部URLを開く方法は別の記事にまとめています。

気をつける点

サフィックス付きのバージョンはパースできない

Versionが扱えるのは数値とピリオドだけです。 セマンティックバージョニングでよく使う-betaのようなサフィックスが付いていると、 パースに失敗します。

Debug.Log(Version.TryParse("1.0.0", out _));       // True
Debug.Log(Version.TryParse("1.0.0-beta", out _));  // False
Debug.Log(Version.TryParse("1.0.0f1", out _));     // False
Debug.Log(Version.TryParse("1", out _));           // False

最後の"1"にも注意してください。 Versionは最低でもMajor.Minorの2要素を必要とするため、 数値1つだけの文字列は変換できません。

Application.versionはPlayer Settingsに入力した文字列がそのまま返ってくるので、 開発中の目印としてサフィックスを付けていると、そのままパースに失敗します。 バージョン欄には数値とピリオドだけを入れる運用にしておきましょう。

versionCodeやBuild番号とは別物

Application.versionと、AndroidのversionCodeやiOSのBuildは別の値です。

versionCodeやBuildはストアがアップロードの新しさを判断するための整数で、ユーザーには基本的に見えません。 一方Application.versionはストアの商品ページに表示される、ユーザー向けの表記です。

アップデート判定を作るときは、 どちらの数値を基準にするのかを最初に決めておいてください。 両者は連動していないので、混ぜて扱うと判定がずれます。

まとめ

バージョンを文字列のまま比較すると、1.9.0から1.10.0に上げた瞬間に壊れる不具合を作り込んでしまいます。 Versionに変換してから比較すれば、この問題は起きません。

気をつけたいのは以下の点です。

  • 外部から来た文字列はTryParseで受ける
  • 指定しなかった要素は-1になるので比較する両者の桁数を揃える
  • そしてサフィックス付きのバージョンは扱えない

アプリの規模が大きくなるほど、バージョン判定のミスは「特定バージョンのユーザーだけ起動できない」という再現しづらい不具合になって返ってきます。 標準クラスで簡単に避けられる部分なので、早めに整えておくのがおすすめです。