LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-16

コードレベルの互換性を保ちつつ引数を増やす方法として省略可能引数を利用するという方法があります。ですが、末尾に 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 を指定したいのは特定の値のケースのみという事情があったとします。つまり、既存の呼び出し箇所においては、foobarcancellationToken のみを指定した呼び出しをしたくて、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();
    }
}

image.png

思うようにいかないというのは、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);
}

image.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 が仮想メソッドでオーバーライドされている可能性がある場合は挙動に差異が出る可能性があるなど、厳密には等価ではないため注意が必要です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0