白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

デフォルト引数があってもシグネチャが変わるから互換性はないよ

こんばんは。
暑いです…四月なのに…。

さて、すごく久しぶりにC#のネタです。

ライブラリってバージョンアップするよね

C#のパッケージマネージャといえばNuGetですが、1.0.0のようにx.y.z形式でバージョニングします。
後方互換のないバージョンアップはメジャーバージョンのxを、後方互換性のある機能追加はマイナーバージョンのyを、後方互換性のあるバグ修正などはパッチバージョンzを上げます。

こういうバージョニングを
セマンティックバージョニング - Wikipedia
といいます。

NuGetサーバ用意するの面倒なので

こんな感じの構成を作ります。

OldLibraryとNewLibraryはアセンブリ名をLibにして、どちらもLib.dllができるようにします。
SampleはLib.dllをプロジェクト参照ではなくアセンブリとして参照している状態です。
これでOldLibraryのdll参照時が古いバージョン(1.0.0)、NewLibrary参照時が新しいバージョン(1.1.0)ということにしましょう。

で、さらにこうします。

もう一つライブラリが増えました。
このとき、OtherLibraryはOldLibraryのLib.dllを、SampleはNewLibraryのLib.dllを参照させます。
これは例えばOtherLibraryの依存関係がLib (>= 1.0.0)とかにLibのバージョンを1.1.0にバージョンアップしたような状態です。


メジャーバージョンあげるのって面倒だよね

さて、準備ができたのでOldLibraryとNewLibraryをバージョンアップ前後の状態にしていきましょう。
すごく簡単なクラスを作って、メソッド2に任意の引数を受けられるように改修したいとします。

// ver. 1.0.0
public class Library
{
    public static string Method1()
    {
        return "Hello World";
    }

    public static string Method2()
    {
        return "Hello World";
    }
}

こんな感じ。利用側でメソッドシグネチャが変わると面倒なのでデフォルト引数をつけてあげることにしました。
これで互換性があるのでマイナーバージョンアップで済みそうです。

// ver. 1.1.0
public class Library
{
    public static string Method1()
    {
        return "Hello World";
    }

    public static string Method2(string target = "World")
    {
        return $"Hello {target}";
    }
}

せっかくなのでユニットテストも書きましょう。ばっちりOKです。

[TestMethod]
public void TestMethod1()
{
    Assert.AreEqual("Hello World", Library.Method1());
}

[TestMethod]
public void TestMethod2()
{
    Assert.AreEqual("Hello World", Library.Method2());
}

[TestMethod]
public void TestMethod2_引数あり()
{
    Assert.AreEqual("Hello Universe", Library.Method2("Universe"));
}

なんかエラーになるぞ

次にOtherLibraryの定義をこんな感じにします。

public class OtherLibrary
{
    public static string Method1()
    {
        return Library.Method1();
    }

    public static string Method2()
    {
        return Library.Method2();
    }
}

Sampleの定義をこんな感じにします。

public class Class
{
    public static string Method1()
    {
        return Library.Method1();
    }

    public static string Method2()
    {
        return Library.Method2();
    }

    public static string Method3()
    {
        return OtherLibrary.Method1();
    }

    public static string Method4()
    {
        return OtherLibrary.Method2();
    }
}

で、テストを書きます。

[TestMethod]
public void TestMethod4()
{
    Assert.AreEqual("Hello World", Class.Method1());
}

// こっちも結果的に同じメソッドを同じように呼んでいるのに
[TestMethod]
public void TestMethod5()
{
    Assert.AreEqual("Hello World", Class.Method2());
}

[TestMethod]
public void TestMethod6()
{
    Assert.AreEqual("Hello World", Class.Method3());
}

// なぜかこっちだけエラーになる
[TestMethod]
public void TestMethod7()
{
    Assert.AreEqual("Hello World", Class.Method4());
}

何故だかOtherLibrary経由のMethod2の呼び出しだけエラーになってしまいました。
エラーの原因を見てみます。どうやらメソッドが見つからないといわれているようです。

System.MissingMethodException: Method not found: 'System.String Lib.Library.Method2()'.

はい。というわけでデフォルト引数はシグネチャが変わります。なので互換性がありません。
直接使う分にはあたかも互換性があるように見えるのが厄介なところですね。

バージョン設定は慎重に

メジャーバージョンアップとマイナーバージョンアップを間違えると怖いよねって話でした。
そしてメジャーバージョンアップは慎重にね。

それでは。