オーバーロード候補の絞り込み

Ver. 7.3

C# 7.3で、オーバーロード解決の改善がありました。 以下の3つの改善があります。

  • 静的メソッドかインスタンス メソッドかの違いで解決できるようになった
  • ジェネリック型制約の違いで解決できるようになった
  • メソッド グループを引数にするとき、メソッドの戻り値を見るようになった

実のところ、これらの改善は「処理手順の順序変更」だそうです。 (今までも、これからも)オーバーロード解決に際して、C# コンパイラーは以下の2つの処理を行っていますが、 この順序を入れ替えることで上記のような区別がつくようになります。

  1. 前述のような、引数の数や型の一致度を調べて最も一致するものを探す
  2. 本当にそのメソッドを呼べるかどうかを調べる(上記の、静的/インスタンスの差や、型制約を調べる)

例えば、以下のコードを見てください。 同名の静的メソッドとインスタンス メソッドを1つずつ定義していますが、 間違った引数で呼び出しています。

using System;

struct Static { }
struct Instance { }

class Program
{
    // 同名で、片方は静的メソッドで、もう片方はインスタンス メソッド。
    static void M(Static x) => Console.WriteLine("Static");
    void M(Instance x) => Console.WriteLine("Instance");

    static void Main()
    {
        // 型名.M() で呼べるのは静的メソッドだけのはず。
        // でも、エラー メッセージとしては「M(Instance) を呼ぶにはインスタンスが必要」の類。
        Program.M(new Instance());

        // インスタンス.M() で呼べるのはインスタンス メソッドだけのはず。
        // でも、エラー メッセージとしては「M(Static) を呼ぶにはインスタンス越しじゃダメ」の類。
        new Program().M(new Static());

        // つまり、引数の型でのオーバーロード解決を先にやって、その後、静的/インスタンスの区別を調べてる。
    }
}

静的かインスタンスかの差をよりも先に、引数の型だけでオーバーロード解決しています。 なので、Program.M(new Instance())と呼ぼうが、M(Instance x)の方がまず選ばれます。 そして、「M(Instance x)はインスタンス メソッドなので、型名.Mでは呼べない」というエラーになります。

C# 7.3でこの順を逆にして、引数の型でオーバーロード解決をする前に、静的かインスタンスかなどの条件を先に見るようになりました。 呼べないことがわかるんだったら最初からオーバーロード解決候補から外して欲しいわけで、 ある意味当然の変更でしょう。

静的メソッドかインスタンス メソッドか

前節の例に、引数の既定値を足してみましょう。 2つのメソッドMが、どちらもM()で呼べるようになります。 C# 7.3からは、これらの呼び分けができるようになりました。

using System;

struct Static { }
struct Instance { }

class Program
{
    // 既定値が入っているのでどちらも M() で呼べる。
    // 片方は静的メソッドで、もう片方はインスタンス メソッド。
    static void M(Static x = default) => Console.WriteLine("Static");
    void M(Instance x = default) => Console.WriteLine("Instance");

    static void Main()
    {
        // 型名.M() で呼べるのは静的メソッドだけのはず。
        // でも、これまでは、M(Static) か M(Instance) かの区別がつかなかった。
        // C# 7.3 では M(Static) が選ばれるように。
        Program.M();

        // インスタンス.M() で呼べるのはインスタンス メソッドだけのはず。
        // 同上。
        // C# 7.3 では M(Instance) が選ばれるように。
        new Program().M();

        // Main が静的メソッドなので、何もつけない場合、この M() も静的な方が呼ばれる。
        M();
    }

    void InstanceMethod()
    {
        // でも、これはダメ。
        // 静的な方もインスタンスの方も M() で呼べるので不明瞭。
        M();

        // これなら OK。
        // this. が付いているのでインスタンス メソッドに絞られる。
        this.M();
    }
}

余談: Color Color 問題

C# では、型名とプロパティ名が同じプロパティを作ることができます。 もっともありがちな例が「Color構造体型のColorプロパティ」なので、「Color Color問題」と呼ばれます。

C# 7.3での静的メソッドとインスタンス メソッドの呼び分けによって、 Color Color問題下においても呼び分けできるようになったものもあります。 しかし、C# 7.3でも解決できないものもあります。

例えば以下の例の通りです。 末尾の2つはC# 7.3でだけコンパイルできるコード、 真ん中の Color.M() はC# 7.3でもコンパイルできないコードになります。

using System;

struct Color
{
    public byte R;
    public byte G;
    public byte B;

    // どちらも M() で呼べるメソッド。
    public void M(int x = 0) => Console.WriteLine("Instance");
    public static void M(Color c = default) => Console.WriteLine("static");

    // 参考までに、オーバーロードがない場合。
    public void Instance() { }
    public static void Static() { }
}

class Program
{
    // C# では、型名とプロパティ名が同じプロパティを作れる。
    static Color Color { get; set; }

    static void Main()
    {
        // これは「プロパティのColor」(C# 7.2以前でも行ける)。
        Color.Instance();

        // これが「型のColor」(C# 7.2以前でも行ける)。
        Color.Static();

        // これだと、この Color が型名かプロパティかが区別できない。
        // C# 7.3 でも不明瞭エラー。
        Color.M();

        // C# 7.3 なら、以下の書き方で呼び分け可能(これまでは不明瞭エラー)。
        // 「プロパティのColor」。
        Program.Color.M();
        // 「型のColor」。
        global::Color.M();
    }
}

ジェネリック型制約

ジェネリック メソッドで、型制約だけが違うメソッドのオーバーロード解決ができるようにもなりました。

using System;

// オーバーロード用のダミー型
struct A { }
struct B { }

// IDisposable, IComparable な型を用意
struct Disposable : IDisposable { public void Dispose() { } }
struct Comparable : IComparable { public int CompareTo(object x) => 0; }

class Program
{
    // M(x) で呼べるメソッドが2つ。
    // 差は、T の型制約のみ。
    static void M<T>(T x, A _ = default) where T : IDisposable { }
    static void M<T>(T x, B _ = default) where T : IComparable { }

    static void Main()
    {
        // C# 7.3 からこの呼び出し方ができるように。
        M(new Disposable());
        M(new Comparable());

        // この書き方も C# 7.3 から。
        M(new Disposable(), default); // default は default(A) に推論される
        M(new Comparable(), default); // default は default(B) に推論される

        // C# 7.2 以前の場合、こう書くのが必須。
        M(new Disposable(), default(A));
        M(new Comparable(), default(B));
    }
}

特に、参照型(class)か値型(struct)かによるオーバーロード解決は便利そうです。 例えば、「条件を満たさなければnullを返す」みたいなメソッドを書きたい場合、 値型の時だけnull許容型にして、?を付ける必要があります。 この呼び分けが、これまでだとなかなか難しかったですが、C# 7.3ではできるようになります。

using System.Collections.Generic;
using System.Linq;

static class ClassExtensions
{
    // クラスの場合は LINQ の FirstOrDefault そのまま。
    public static T FirstOrNull<T>(this IEnumerable<T> source)
        where T : class
        => source.FirstOrDefault();
}

static class StructExtensions
{
    // 構造体の場合は null 許容型に変える必要がある。
    public static T? FirstOrNull<T>(this IEnumerable<T> source)
        where T : struct
        => source.Select(x => (T?)x).FirstOrDefault();
}

class Program
{
    static void Main()
    {
        // ClassExtensions の方のが呼ばれる。
        new[] { "a", "b", "c" }.FirstOrNull();

        // StructExtensions の方のが呼ばれる。
        new[] { 1, 2, 3 }.FirstOrNull();
    }
}

メソッドの戻り値

C# (というか、.NET)のメソッドは、戻り値の型をシグネチャに含みません。 基本的に、戻り値だけが違うメソッドは定義できませんし、呼び分けもできません。

ただ、これまでの例でもたびたび出てきたように、引数の規定値を与えることで戻り値だけが違う「っぽく見える」メソッド オーバーロードはできます。 また、以下のように、「戻り値違いのデリゲートを受け取るメソッド」は作れます。

static void M(Func<int> f) => Console.WriteLine("int");
static void M(Func<string> f) => Console.WriteLine("string");

前述の通り、 ラムダ式であれば、ラムダ式の型推論が賢くて、この2つのメソッドの呼び分けができました。

M(() => 0); // int の方
M(() => "abc"); // string の方

しかし、メソッド グループを引数に渡した場合、これまではオーバーロード解決できませんでした。 それが、以下のように、C# 7.3からはオーバーロード解決できるようになります。

using System;

class Program
{
    static void M(Func<int> f) => Console.WriteLine("int");
    static void M(Func<string> f) => Console.WriteLine("string");

    static int IntReturn() => 0;
    static string StringReturn() => "";

    static void Main()
    {
        // ラムダ式賢い。
        M(() => 0); // int の方
        M(() => "abc"); // string の方

        // こういう書き方なら C# 7.2 まででもできた。
        M(() => IntReturn());
        M(() => StringReturn());

        // なのに、以下のような書き方はこれまでできなかった。
        // C# 7.3 からできるように。
        M(IntReturn);
        M(StringReturn);
    }
}

余談: 同一シグネチャのメソッド オーバーロード

ここで説明してきたように、C# 7.3から静的メソッドかインスタンス メソッドかや、 ジェネリック型制約の差でオーバーロード解決できるようになりました。

呼び分けできるようになったんなら、そもそもオーバーロードもできていいはずではあります。 しかし、静的/インスタンス違いや型制約違いでオーバーロードを作れないのは、 C# ではなく、.NET 型システムの制約です。 単に C# コンパイラーだけの仕事ではないので、これを修正するのは少し難しいです。 そのため、これは引き続き認められていません。

// 以下の2つは呼び分けできるようになった。
// なのに、定義はできない(C# コンパイラーだけの問題じゃないので直せない)。
static void M<T>(T x) where T : struct { }
static void M<T>(T x) where T : class { }

ただし、これまで挙げてきた例で少し出てきていますが、 「ごまかす」方法がいくつかあります。

1つはオプション引数(引数の規定値)や可変長引数を使う方法で、以下のような書き方で「違うオーバーロードなんだけど、実質的には同じ呼び方ができる」と言うようなメソッドを定義できます。

class Program
{
    // 呼び分け用のダミー型
    struct Struct { }
    struct Class { }

    // ダミー引数を足すことでオーバーロードする。
    static void M<T>(T x, Struct _ = default) where T : struct { }
    static void M<T>(T x, Class _ = default) where T : class { }

    static void Main()
    {
        M(1);     // M(T, Struct) が呼ばれる
        M("abc"); // M(T, Class) が呼ばれる
    }
}

もう1つは拡張メソッドを使う方法です。 拡張メソッドであれば、別のクラス中で定義してやれば、同じ型を対象とした全く同じシグネチャのメソッドを定義できます。

using System.Collections.Generic;
using System.Linq;

static class ClassExtensions
{
    public static T FirstOrNull<T>(this IEnumerable<T> source)
        where T : class
        => source.FirstOrDefault();
}

static class StructExtensions
{
    public static T? FirstOrNull<T>(this IEnumerable<T> source)
        where T : struct
        => source.Select(x => (T?)x).FirstOrDefault();
}

また、refの有無が違うだけの拡張メソッドでもオーバーロード可能です。

static class Extensions
{
    // ref の有無の差 + 型制約
    public static void M<T>(this ref T x) where T : struct { }
    public static void M<T>(this T x) where T : class { }
}

class Program
{
    static void Main()
    {
        "abc".M();

        var x = 123;
        x.M();
        // ただ、ref 拡張メソッドの性質上、123.M() とは呼べない(リテラルがダメ)
        // また、DateTime.Now.M() とかもダメ(プロパティ越しがダメ)
    }
}

いずれも疑似的なもので、ダミーなしのオーバーロードと比べると利便性は下がりますが、 C# 7.3で呼び分けができるようになったことで、少し使い勝手はよくなりました。

更新履歴

ブログ