引数の途中に省略可能引数を増やして元のメソッドは Obsolete にする

コードレベルの互換性を保ちつつ引数を増やす方法として省略可能引数を利用するという方法があります。ですが、末尾に `CancellationToken` を受け取りたい場合など、引数の最後に省略可能引数を追加できず、途中に追加したいというケースもなくはないと思います。そういった場合に、新たなシグネチャのメソッドに段階的に移行するための Tips をご紹介します。[^1]

# 本題

次のようなソースがあったとします。

```csharp
var hoge = new Hoge();
await hoge.DoFugaAsync("a", 1, CancellationToken.None);

public class Hoge
{
    public async Task DoFugaAsync(string foo, int bar, CancellationToken cancellationToken)
    {
        await Task.Yield();
    }
}
```

ここで、ある機能拡張によって `int bar` の後ろに `bool? baz = null` を増やしたいとします。

ただし、省略可能引数としているように、`baz` を指定したいのは特定の値のケースのみという事情があったとします。つまり、既存の呼び出し箇所においては、`foo` と `bar` と `cancellationToken` のみを指定した呼び出しをしたくて、`baz` は必要に応じて指定したいとします。

一方で、

```csharp
Task DoFugaAsync(string foo, int bar, CancellationToken cancellationToken);
Task DoFugaAsync(string foo, int bar, bool? baz = null, CancellationToken cancellationToken);
```

なるオーバーロードが存在するのは紛らわしく煩雑なので、前者（旧シグネチャ）は `Obsolete` にして、後者（新シグネチャ）へ段階移行したいというシーンを考えてください。[^2]

このとき、普通にオーバーロードの片方を `Obsolete` にしても思うようにいきません。

```csharp
var hoge = new Hoge();
await hoge.DoFugaAsync("a", 1, cancellationToken: CancellationToken.None); // baz を指定しないと警告が出る
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//

public class Hoge
{
    [Obsolete]
    public Task DoFugaAsync(string foo, int bar, CancellationToken cancellationToken)
        => DoFugaAsync(foo, bar, null, cancellationToken);

    public async Task DoFugaAsync(string foo, int bar, bool? baz = null, CancellationToken cancellationToken = default)
    {
        await Task.Yield();
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11978/19da8721-6432-5e28-660b-72a299e97e1b.png)


思うようにいかないというのは、`baz` を指定しないと `Obsolete` 側のオーバーロードへ呼び出しが解決されてしまうということです。

これは、オーバーロード解決の優先順として[「引数の数が一致するもの」が優先されてしまう](https://ufcpp.net/study/csharp/structured/miscoverloadresolution/#betterness-rule)ためです。

やりたいことは

- `baz` が必要ないケースでは指定しないで済ませたい
- 従来のメソッドは `Obsolete` にして新メソッドへ移行を促したい
- すなわち、`cancellationToken: ` を付けた個所は移行完了とし、そうでない箇所は警告としたい

ということです。

これを実現するには、オーバーロード解決のルール[インスタンスメソッド優先](https://ufcpp.net/study/csharp/structured/miscoverloadresolution/#instance)を利用します。

```csharp
var hoge = new Hoge();
await hoge.DoFugaAsync("a", 1, CancellationToken.None); // 警告
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~//
await hoge.DoFugaAsync("a", 1, cancellationToken: CancellationToken.None); // 警告されない（インスタンスメソッド側が呼ばれる）

public class Hoge
{
    public async Task DoFugaAsync(string foo, int bar, bool? baz = null, CancellationToken cancellationToken = default)
    {
        await Task.Yield();
    }
}

public static class HogeExtensions
{
    // Obsolete 側を拡張メソッドにして優先順位を下げる
    [Obsolete]
    public static Task DoFugaAsync(this Hoge hoge, string foo, int bar, CancellationToken cancellationToken)
        => hoge.DoFugaAsync(foo, bar, null, cancellationToken);
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11978/0b6d62b3-626f-66d9-e1d1-5e233bacbf76.png)


このように旧シグネチャは拡張メソッドとすることで、名前付き引数にしない状態では `Obsolete` な拡張メソッドが呼ばれ、名前付き引数で `CancellationToken` を渡すことでインスタンスメソッドである新メソッドが呼ばれるようになります。

一点、この方法はコードレベルの互換性はあるものの、アセンブリレベルでの互換性は維持されないため、メソッドの定義側と呼び出し側でアセンブリが分かれている、かつ定義側のアセンブリのみを差し替えたいケースでは利用できません。`MissingMethodException` がスローされる（はず）ので気を付けましょう。[^3]

----
[^1]: これはあくまで私が突発的に考えた方法なので、もしかしたらもっと良いやり方があるかもしれません。コメントお待ちしております。
[^2]: 今回であれば `Task DoFugaAsync(string foo, int bar, CancellationToken cancellationToken)` と `Task DoFugaAsync(string foo, int bar, bool baz, CancellationToken cancellationToken)` のオーバーロードでいいじゃないかという意見もあると思いますが、例えば `bool? baz = null, bool? qux = null, string quux = null` のように複数の省略可能引数を追加したく、かつその一部だけ指定するケースがある、などの場合にはオーバーロードでの対応は難しいと思います。また、引数を `FugaParameter` のようなクラスにする方法もあります。今回はクラスを定義せずに省略可能引数を使いたいケースを想定しています。
[^3]: 他にも、`DoFugaAsync` が仮想メソッドでオーバーライドされている可能性がある場合は挙動に差異が出る可能性があるなど、厳密には等価ではないため注意が必要です。