次の方法で共有


C# でのバージョン管理

このチュートリアルでは、.NET でのバージョン管理のしくみについて学習します。 また、ライブラリのバージョン管理を行う際や、ライブラリを新しいバージョンにアップグレードする際の考慮事項についても学習します。

Language version (言語バージョン)

C# コンパイラは .NET SDK の一部です。 既定では、コンパイラは、プロジェクトに対して選択した TFM に一致する C# 言語バージョンを選択します。 SDK のバージョンが選択したフレームワークより大きい場合、コンパイラではより大きな言語バージョンを使用できます。 プロジェクトの LangVersion 要素を設定することで、既定を変更できます。 方法についてはコンパイラ オプションに関する記事を参照してください。

警告

LangVersion 要素を latest に設定することは推奨されていません。 latest 設定にすると、インストールされているコンパイラで最新バージョンが使用されます。 これはマシンによって変わり、ビルドの信頼性が低下する可能性があります。 さらに、現在の SDK に含まれていないランタイム機能またはライブラリ機能を必要とする可能性がある言語機能も有効になります。

ライブラリの作成

一般用途向けの .NET ライブラリを作成したことがある開発者であれば、新しい更新プログラムをロールアウトする必要に迫られた経験もあることでしょう。 このプロセスのあり方によって、既存のコードを新バージョンのライブラリへとシームレスに移行できるかどうかは大きく左右されます。 ここでは、新しいリリースを作成する際に考慮すべき点について、いくつか説明します。

セマンティック バージョニング

セマンティック バージョン管理 (略して SemVer) は、特定のマイルス トーン イベントを示すためにライブラリの各バージョンに適用される命名規則です。 うまく管理すれば、ライブラリに適用されたバージョン情報によって、同じライブラリの旧バージョンを使用したプロジェクトとの互換性を開発者が確認できるようになります。

SemVer に対する最も基本的なアプローチは、3 コンポーネント形式 MAJOR.MINOR.PATCH です。

  • MAJOR は、互換性のない API 変更を加えたときにインクリメントされます
  • MINOR は、下位互換性のある方法で機能を追加したときにインクリメントされます
  • PATCH は、下位互換性のあるバグ修正を行ったときにインクリメントされます

例を使用してバージョンの増分を理解する

各バージョン番号をインクリメントするタイミングを明確にするために、具体的な例を次に示します。

MAJOR バージョン更新(互換性を持たない API の変更)

これらの変更により、ユーザーは新しいバージョンで動作するようにコードを変更する必要があります。

  • パブリック メソッドまたはプロパティの削除:

    // Version 1.0.0
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Subtract(int a, int b) => a - b; // This method exists
    }
    
    // Version 2.0.0 - MAJOR increment required
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        // Subtract method removed - breaking change!
    }
    
  • メソッド シグネチャの変更:

    // Version 1.0.0
    public void SaveFile(string filename) { }
    
    // Version 2.0.0 - MAJOR increment required
    public void SaveFile(string filename, bool overwrite) { } // Added required parameter
    
  • 期待を破る方法で既存のメソッドの動作を変更する:

    // Version 1.0.0 - returns null when file not found
    public string ReadFile(string path) => File.Exists(path) ? File.ReadAllText(path) : null;
    
    // Version 2.0.0 - MAJOR increment required
    public string ReadFile(string path) => File.ReadAllText(path); // Now throws exception when file not found
    

MINOR バージョンの更新 (後方互換性のある機能)

これらの変更により、既存のコードを中断することなく新しい機能が追加されます。

  • 新しいパブリック メソッドまたはプロパティの追加:

    // Version 1.0.0
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
    }
    
    // Version 1.1.0 - MINOR increment
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Multiply(int a, int b) => a * b; // New method added
    }
    
  • 新しいオーバーロードを追加すること

    // Version 1.0.0
    public void Log(string message) { }
    
    // Version 1.1.0 - MINOR increment
    public void Log(string message) { } // Original method unchanged
    public void Log(string message, LogLevel level) { } // New overload added
    
  • 既存のメソッドへの省略可能なパラメーターの追加:

    // Version 1.0.0
    public void SaveFile(string filename) { }
    
    // Version 1.1.0 - MINOR increment
    public void SaveFile(string filename, bool overwrite = false) { } // Optional parameter
    

    これは ソース互換の変更ですが、 バイナリ破壊的変更です。 このライブラリのユーザーが正常に動作するためには、再コンパイルする必要があります。 多くのライブラリでは、マイナー バージョンの変更ではなく、メジャー バージョンの変更でのみこれを考慮します。

PATCH バージョンの更新 (下位互換性のあるバグ修正)

これらの変更により、新しい機能を追加したり、既存の機能を中断したりすることなく、問題が修正されます。

  • 既存のメソッドの実装のバグを修正する:

    // Version 1.0.0 - has a bug
    public int Divide(int a, int b)
    {
        return a / b; // Bug: doesn't handle division by zero
    }
    
    // Version 1.0.1 - PATCH increment
    public int Divide(int a, int b)
    {
        if (b == 0) throw new ArgumentException("Cannot divide by zero");
        return a / b; // Bug fixed, behavior improved but API unchanged
    }
    
  • API を変更しないパフォーマンスの向上:

    // Version 1.0.0
    public List<int> SortNumbers(List<int> numbers)
    {
        return numbers.OrderBy(x => x).ToList(); // Slower implementation
    }
    
    // Version 1.0.1 - PATCH increment
    public List<int> SortNumbers(List<int> numbers)
    {
        var result = new List<int>(numbers);
        result.Sort(); // Faster implementation, same API
        return result;
    }
    

重要な原則は、既存のコードで新しいバージョンを変更なしで使用できる場合は、MINOR または PATCH 更新です。 新しいバージョンで動作するように既存のコードを変更する必要がある場合は、メジャー更新プログラムです。

.NET ライブラリにバージョン情報を適用する際には、他のシナリオ (プレリリース バージョンなど) を指定することもできます。

下位互換性

ライブラリの新バージョンをリリースする際、特に大きな懸念事項となるのが、旧バージョンとの互換性です。 旧バージョンに依存するコードが再コンパイル後に新バージョンで機能する場合、ライブラリの新バージョンは旧バージョンに対してソース互換性があるということになります。 旧バージョンに依存するアプリケーションが再コンパイルを経ずに新バージョンで機能する場合、ライブラリの新バージョンはバイナリ互換性があるということになります。

次に示すのは、旧バージョンのライブラリとの下位互換性を維持するうえでの考慮事項です。

  • 仮想メソッド: 新バージョンで仮想メソッド非仮想にした場合は、そのメソッドをオーバーライドするプロジェクトを更新する必要があります。 これはきわめて重大な変更であり、極力回避することをお勧めします。
  • メソッド シグネチャ: メソッドの動作を更新するためにそのシグネチャも変更する必要がある場合は、そのメソッドに対するコード呼び出しが引き続き機能するように、代わりにオーバーロードを作成する必要があります。 旧メソッドのシグネチャは、新しいメソッド シグネチャを呼び出すようにいつでも操作して、実装の整合性を維持できます。
  • Obsolete 属性: この属性をコード内で使用すると、現在非推奨に指定されていて、今後のバージョンで削除される可能性が高いクラスやクラス メンバーを指定することができます。 これにより、ライブラリを使用している開発者が、今後の重大な変更に余裕を持って準備できるようになります。
  • 省略可能なメソッド引数: これまで省略可能であったメソッド引数を必須にしたり、それらの既定値を変更する場合は、それらの引数が指定されていないすべてのコードを更新する必要があります。

必須の引数を省略可能にしても、メソッドの動作が変更されない限り、影響はほとんどありません。

新バージョンのライブラリへの更新を行いやすくすれば、その分、ユーザーがアップグレードを早く完了できるようになります。

アプリケーション構成ファイル

.NET 開発者の皆さんは、ほとんどのタイプのプロジェクトで app.config ファイルを使用しているのではないでしょうか。 このシンプルな構成ファイルは、新しい更新プログラムのロールアウトをスムーズにするうえで大いに役立ちます。 通常、ライブラリを設計する際には、定期的に変更される可能性が高い情報を app.config ファイルに保存します。そうすれば、それらの情報が更新された際にも、ライブラリの再コンパイルを行うことなく、旧バージョンの構成ファイルを新しいバージョンに置き換えるだけで済みます。

ライブラリの使用

他の開発者によって作成された .NET ライブラリを使用する場合には、新バージョンのライブラリが自分のプロジェクトに対して完全互換ではない場合が多く、それらの変更にうまく対応するために、コードを更新しなければならないことも少なくありません。

幸いなことに、C# と .NET エコシステムでは、重大な変更をもたらす可能性がある新バージョンのライブラリと正常に連携できるよう、アプリを簡単に更新するための機能や技術が提供されています。

アセンブリ バインド リダイレクト

app.config ファイルを使用して、アプリで使用するライブラリのバージョンを更新できます。 バインド リダイレクトというものを追加することで、アプリを再コンパイルしなくても、新しいライブラリ バージョンを使用することができます。 次の例は、アプリの app.config ファイルを更新して、当初のコンパイルに使用された 1.0.1 バージョンではなく、ReferencedLibrary パッチ バージョンの 1.0.0 が使用されるようにする方法を示しています。

<dependentAssembly>
    <assemblyIdentity name="ReferencedLibrary" publicKeyToken="32ab4ba45e0a69a1" culture="en-us" />
    <bindingRedirect oldVersion="1.0.0" newVersion="1.0.1" />
</dependentAssembly>

このアプローチは、新バージョンの ReferencedLibrary がアプリに対してバイナリ互換性を持っている場合にのみ有効です。 互換性を判断するときに注意すべき変更点については、上記の「下位互換性」セクションをご覧ください。

新規

new 修飾子を使用して、基底クラスの継承メンバーを非表示にできます。 これは、派生クラスが基底クラスの更新に対応できるようにするための 1 つの手段です。

次に例を示します。

public class BaseClass
{
    public void MyMethod()
    {
        Console.WriteLine("A base method");
    }
}

public class DerivedClass : BaseClass
{
    public new void MyMethod()
    {
        Console.WriteLine("A derived method");
    }
}

public static void Main()
{
    BaseClass b = new BaseClass();
    DerivedClass d = new DerivedClass();

    b.MyMethod();
    d.MyMethod();
}

出力

A base method
A derived method

上記の例では、DerivedClass によって MyMethod 内の BaseClass メソッドを非表示にしています。 つまり、派生クラス内に既に存在しているメンバーが新バージョンのライブラリ内の基底クラスによって追加された場合には、派生クラスのメンバーに new 修飾子を使用するだけで、基底クラスのメンバーを非表示にすることができます。

new 修飾子が指定されなかった場合、派生クラスは既定で基底クラスの競合メンバーを非表示にします。コンパイラの警告が生成されますが、コードはコンパイルされます。 つまり、既存のクラスに新しいメンバーを追加するだけで、新バージョンのライブラリは依存先のコードに対して、ソースとバイナリの両方の互換性を持つことになります。

オーバーライド

override 修飾子を使用した場合、派生実装は基底クラス メンバーの実装を非表示にはせず、そのメンバーを拡張します。 基底クラスのメンバーには、virtual 修飾子が適用されている必要があります。

public class MyBaseClass
{
    public virtual string MethodOne()
    {
        return "Method One";
    }
}

public class MyDerivedClass : MyBaseClass
{
    public override string MethodOne()
    {
        return "Derived Method One";
    }
}

public static void Main()
{
    MyBaseClass b = new MyBaseClass();
    MyDerivedClass d = new MyDerivedClass();

    Console.WriteLine($"Base Method One: {b.MethodOne()}");
    Console.WriteLine($"Derived Method One: {d.MethodOne()}");
}

出力

Base Method One: Method One
Derived Method One: Derived Method One

override 修飾子はコンパイル時に評価され、オーバーライドする仮想メンバーが見つからない場合には、コンパイラがエラーをスローします。

説明されている手法の知識を持ち、それらを使用する状況を理解しておくと、ライブラリのバージョン間の移行を容易にするために今後も役立ちます。