滅入るんるん

何か書きます

【C#】デフォルト引数のバージョニング問題

.NETのクラスライブラリ設計 開発チーム直伝の設計原則,コーディング標準,パターン

.NETのクラスライブラリ設計 開発チーム直伝の設計原則,コーディング標準,パターン

『.NETのクラスライブラリ設計』という本を読んでいて、デフォルト引数はバージョニング問題があるので(ライブラリでの)使用は避けてくださいみたいな記述があり、なるほどなぁとなったので技術的検証とかしてみたので書いておきます。

ちなみに、実験プロジェクトはGithubのほうに公開しておきます。

github.com

バージョニング問題とは

バージョニング問題で有名なのはconstのほうですね。

コンパイル時に固定値を埋め込むので、その固定値を変更してしまうとそのアセンブリを参照してるコードすべてをコンパイルしなおさないといけないというものです。

using System;

namespace DefaultParameterSample.Core
{
    public class Sample
    {
        public const string ConstString = "const_string: 1";
        public static readonly string ReadonlyString = ConstString;

        public void Method(string value = ConstString)
        {
            Console.WriteLine(value);
        }
    }
}

例を挙げると、このコードはコンパイル時にReadonlyStringの値にconst_string: 1が仕込まれます。

これは他のアセンブリでも同様に行われます。

using DefaultParameterSample.Core;

namespace DefaultParameterSample.Console
{
    class Program
    {
        static void Main(string[] args)
        {
            var sample = new Sample();

            System.Console.WriteLine($"--- {nameof(Sample.ConstString)} ---");
            System.Console.WriteLine(Sample.ConstString);
            System.Console.WriteLine($"--- {nameof(Sample.ReadonlyString)} ---");
            System.Console.WriteLine(Sample.ReadonlyString);
            System.Console.WriteLine($"--- {nameof(Sample.Method)} ---");
            sample.Method();

            System.Console.ReadLine();
        }
    }
}

たとえば、別プロジェクトでこのように参照しているコードがあるとしましょう。 Sample.ConstStringはコンパイル時にconst_string: 1の値が仕込まれます。

しかし、このプロジェクトのコンパイル後に元のアセンブリのSample.ConstStringの値が変更され、その変更後のアセンブリを参照していたらどうなるでしょうか。

結果はこちらのプロジェクトのコードに仕込まれた値は変更されずに実行されます。

デフォルト引数もコンパイル時に定数を埋め込む

今回の主題ですが、デフォルト引数もconstのようにコンパイル時に定数を埋め込むような挙動をしているということです。

たとえば、上述のようなコードはCILレベルでは以下のようになります。

f:id:meilcli:20181020194739p:plain

ちょっと見にくいかもしれないのでコードを挙げます。

// sample.Method("const_string: 1");
IL_0055: ldstr "const_string: 1"
IL_005a: callvirt instance void [DefaultParameterSample.Core]DefaultParameterSample.Core.Sample::Method(string)

IL_0055:ldstrは見た目そのまま、右側で宣言されている文字列をスタックに読み込む命令です。この部分がコンパイル時に埋め込まれているということを表すものになります。

実際にバージョニング問題を起こしてみよう

バージョニング問題を起こそうとしても、Visual Studioを使っているとなかなか起こせません。 手元に用意したプロジェクトではすべての参照コードを実行時にコンパイルするみたいで引き起こすことができなかったので、実行可能ファイルを生成し参照しているアセンブリファイルを取り換えるという手段を選びました。

コードは最初に挙げておいたGithubリポジトリのソリューションや、細かいところは前述のサンプルコードになります。

ソリューションを一度ビルドしたら、コマンドラインで\DefaultParameterSample.Consoleに移動します。 Windowsでコンソールアプリケーションを実行したいので、dotnet publish -c Release -r win-x64を実行します。

\DefaultParameterSample.Console\bin\Release\netcoreapp2.1\win-x64\に.exeファイルやその他いろいろが生成されるので、生成されたDefaultParameterSample.Console.exeを実行してみます。

f:id:meilcli:20181020195701p:plain

実行結果は画像のようになります。

次にコードの値を変更してみましょう。

using System;

namespace DefaultParameterSample.Core
{
    public class Sample
    {
        public const string ConstString = "const_string: 2";
        public static readonly string ReadonlyString = ConstString;

        public void Method(string value = ConstString)
        {
            Console.WriteLine(value);
        }
    }
}

今回はConstString = "const_string: 1";ConstString = "const_string: 2";に変えてみました。

ここからDefaultParameterSample.Coreのアセンブリのみを取り換える作業になります。 まずは、Visual StudioなどでDefaultParameterSample.CoreプロジェクトのみをReleaseモードでビルドしてください。

\DefaultParameterSample.Core\bin\Release\netstandard2.0\に新たに生成された.dllファイルなどが存在するので、これを先ほどの\DefaultParameterSample.Console\bin\Release\netcoreapp2.1\win-x64\にコピー&置き換えをして取り替えます。

これでバージョニング問題を再現できたので、DefaultParameterSample.Console.exeを実行します。

f:id:meilcli:20181020195642p:plain

結果は画像のようになり、static readonlyとしたReadonlyString以外のコンパイル時に埋め込まれた値が残っています。

まとめ

  • publicなconstやデフォルト引数は将来にわたって値に変更がないときに使う
  • ライブラリーでの使用はとくに注意したほうがいい
  • 値を変更する可能性がある場合はstatic readonlyにする