コードレベルの互換性を保ちつつ引数を増やす方法として省略可能引数を利用するという方法があります。ですが、末尾に CancellationToken
を受け取りたい場合など、引数の最後に省略可能引数を追加できず、途中に追加したいというケースもなくはないと思います。そういった場合に、新たなシグネチャのメソッドに段階的に移行するための Tips をご紹介します。1
本題
次のようなソースがあったとします。
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
は必要に応じて指定したいとします。
一方で、
Task DoFugaAsync(string foo, int bar, CancellationToken cancellationToken);
Task DoFugaAsync(string foo, int bar, bool? baz = null, CancellationToken cancellationToken);
なるオーバーロードが存在するのは紛らわしく煩雑なので、前者(旧シグネチャ)は Obsolete
にして、後者(新シグネチャ)へ段階移行したいというシーンを考えてください。2
このとき、普通にオーバーロードの片方を Obsolete
にしても思うようにいきません。
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();
}
}
思うようにいかないというのは、baz
を指定しないと Obsolete
側のオーバーロードへ呼び出しが解決されてしまうということです。
これは、オーバーロード解決の優先順として「引数の数が一致するもの」が優先されてしまうためです。
やりたいことは
-
baz
が必要ないケースでは指定しないで済ませたい - 従来のメソッドは
Obsolete
にして新メソッドへ移行を促したい - すなわち、
cancellationToken:
を付けた個所は移行完了とし、そうでない箇所は警告としたい
ということです。
これを実現するには、オーバーロード解決のルールインスタンスメソッド優先を利用します。
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);
}
このように旧シグネチャは拡張メソッドとすることで、名前付き引数にしない状態では Obsolete
な拡張メソッドが呼ばれ、名前付き引数で CancellationToken
を渡すことでインスタンスメソッドである新メソッドが呼ばれるようになります。
一点、この方法はコードレベルの互換性はあるものの、アセンブリレベルでの互換性は維持されないため、メソッドの定義側と呼び出し側でアセンブリが分かれている、かつ定義側のアセンブリのみを差し替えたいケースでは利用できません。MissingMethodException
がスローされる(はず)ので気を付けましょう。3
-
これはあくまで私が突発的に考えた方法なので、もしかしたらもっと良いやり方があるかもしれません。コメントお待ちしております。 ↩
-
今回であれば
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
のようなクラスにする方法もあります。今回はクラスを定義せずに省略可能引数を使いたいケースを想定しています。 ↩ -
他にも、
DoFugaAsync
が仮想メソッドでオーバーライドされている可能性がある場合は挙動に差異が出る可能性があるなど、厳密には等価ではないため注意が必要です。 ↩