目次

キーワード

概要

関数で説明しましたが、 C# では関数メンバーに対して、 同名で引数リストだけが違う物を定義でき、これをオーバーロードと呼びます。

同名の関数がいくつかあるので、M(0) などと書いた時、実際には「どのMが呼ばれるか」という検索処理が必要になります。 このような同名の関数のうちどれを呼ぶか探す処理をオーバーロード解決(overload resolution)と呼びます。

本項では、C# がどういうルールでオーバーロード解決を行っているのかについて説明して行きます。

「より一致度の高いものを選ぶ」ルール

オーバーロード解決は、基本方針だけを一言でいうとシンプルで、 「より一致度の高いものを選ぶ」という方針になっています。 詳しくは後々説明して行くことになりますが、例えば以下のようなルールになっています。

  • 型変換なしで引数に渡せるなら、それを優先的に呼ぶ
  • 引数の数がピッタリ一致している方を優先的に呼ぶ

引数の型

引数の型は、以下のリストの上の方ほど「一致度が高い」と判断されます。

  • ぴったり一致する型
  • ジェネリックな型
  • 親クラス
    • 多段に派生している場合、近い方ほど優先
  • 暗黙的に変換できる型
  • object

型変換なしで渡せるものほど「一致」、 いろんな型を受け付けるものほど「不一致」です。

例えば以下のようなメソッド M を書いた場合、 上の方に書いたものほど優先的に呼ばれます。

using System;

// A → B → C の型階層
// IDisposable インターフェイスを実装
// C には int への暗黙的型変換あり
class A : IDisposable { public void Dispose() { } }
class B : A, IDisposable { }
class C : B, IDisposable
{
    public static implicit operator int(C x) => 0;
}

class Program
{
    static void Main()
    {
        // M のオーバーロードがいくつかある中、C を引数にして呼び出す
        M(new C());
    }

    // 上から順に候補になる。
    // 上の方を消さないと、下の方が呼ばれることはない。

    // 「そのもの」が当然1番一致度高い
    static void M(C x) => Console.WriteLine("C");

    // 次がジェネリックなやつ。型変換が要らないので一致度が高いという扱い。
    static void M<T>(T x) => Console.WriteLine("generic");

    // 基底クラスは、階層が近い方が優先。この場合 B が先で、A が後
    static void M(B x) => Console.WriteLine("B");

    static void M(A x) => Console.WriteLine("A");

    // 次に、インターフェイス、暗黙的型変換が同率。
    // (構造体の時の ValueType と違って、クラスは明確に基底クラスが上。)
    // この2つが同時に候補になってると ambiguous エラー
    static void M(IDisposable x) => Console.WriteLine("IDisposable");
    static void M(int x) => Console.WriteLine("int");

    // 最後が object。
    static void M(object x) => Console.WriteLine("object");
}

型変換に関しては、候補が複数ある場合は、どちらを呼ぶべきか不明瞭なためコンパイル エラーになります。 例えば以下のコードはコンパイルできません。

using System;

// インターフェイス実装とユーザー定義の型変換を持つ
class A : IDisposable
{
    public void Dispose() { }
    public static implicit operator int(A x) => 0;
}

class Program
{
    static void M(IDisposable x) => Console.WriteLine("IDisposable");
    static void M(int x) => Console.WriteLine("int");

    static void Main()
    {
        // インターフェイスへの変換と、ユーザー定義の型変換は同列
        // どちらを呼ぶべきか、このコードでは解決できない
        M(new A());

        // 明示的にキャストを書けば大丈夫
        M((IDisposable)new A());
        M((int)new A());
    }
}

型の派生に関してはクラスのみです。 C# では、任意の値型System.ValueType クラスから派生、任意の列挙型System.Enumクラスから派生しているように振る舞いますが、 これらはあくまで「それっぽく振る舞うようにコンパイラーが特殊対応している」というだけで、 実際には型変換の一種です。 そのため、以下のようなコードはコンパイル エラーになります。

using System;

struct S : IDisposable
{
    public void Dispose() { }
}

class Program
{
    static void Main()
    {
        // S は ValueType から派生しているかのように振る舞うものの、これはあくまで ValueType への型変換になる
        // インターフェイスへの変換と同列なので、以下の呼び出しは不明瞭
        M(new S());
    }

    static void M(IDisposable x) => Console.WriteLine("IDisposable");
    static void M(ValueType x) => Console.WriteLine("ValueType");
}

ジェネリック メソッド

C# では、「ジェネリックかどうか」だけの差があるメソッド オーバーロードも可能です。 この場合、非ジェネリックな方が優先的に呼ばれます。

using System;

class Program
{
    static void Main()
    {
        // M(string) の方が呼ばれる
        M("abc");

        // M<T>(string) の方が呼ばれる
        M<int>("abc");
    }

    static void M(string x) => Console.WriteLine("M");
    static void M<T>(string x) => Console.WriteLine("M<T>");
}

オプション引数・可変長引数

C# にはオプション引数可変長引数という、引数を省略できる仕組みが2つあります。 この場合、以下のリストの上の方ほど「一致度が高い」と判断されます。

  • 省略なくぴったり引数の数が一致しているもの
  • オプション引数による省略
  • 可変長引数による省略
using System;

class Program
{
    static void Main()
    {
        M();
    }

    // これが最優先
    static void M() => Console.WriteLine("void");

    // 次がこれ。既定値を与えたもの
    static void M(int x = 0) => Console.WriteLine("int x = 0");

    // 最後がこれ。params
    static void M(params int[] x) => Console.WriteLine("params int[]");
}

インスタンス メソッド優先

C# には拡張メソッドという、 インスタンス メソッドと同じ書き方で静的メソッドを呼べます。 正確にはオーバーロードとは言わないんですが、 インスタンス メソッドと同名の拡張メソッドも定義できるので、 オーバーロードと同種の「解決」が必要になります。

この場合、インスタンス メソッドの方が優先です。 拡張メソッドの方を呼びたければ、本来の静的メソッドとして呼ぶ必要があります。

using System;

class A
{
    public void M() => Console.WriteLine("instance");
}

static class Extensions
{
    public static void M(this A a) => Console.WriteLine("extension");
}

class Program
{
    static void Main()
    {
        // instance の方が呼ばれる
        new A().M();

        // A 自身が M を持っている以上、↑の書き方で拡張メソッドの方は呼べない
        // 以下のように、普通に静的メソッドとして呼ぶ必要がある
        Extensions.M(new A());
    }
}

型推論とオーバーロード解決

C# の構文にはいくつか、左辺値からの型推論をするものがあります。

推論に推論を重ねることになるので、これらの型を引数にした場合、オーバーロード解決ができない場合が増えます。

using System;

// 引数が完全に一致しているデリゲート型を2個用意
delegate int A(int x);
delegate int B(int x);

class Program
{
    static void Main()
    {
        // 2個以上候補があるときに default は使えない
        M(default);

        // 型推論とはちょっと違うものの、null (型がない。どの型にでも代入可)でも同様
        M(null);

        // 型指定ありの default なら大丈夫
        M(default(A));

        // A なのか B なのか区別がつかない
        M(x => x);

        // キャストがあれば大丈夫
        // new でも可
        M((A)(x => x));
        M(new A(x => x));
    }

    static void M(A x) => Console.WriteLine("A");
    static void M(B x) => Console.WriteLine("A");
}

文字列補完では、string型で受け取る場合とFormattableStringで受け取る場合で異なる挙動になりますが、 varを使った暗黙的変数宣言では自動的にstring扱いされます。 そのため、オーバーロード解決でも特にキャストがない場合、stringが優先されます。

using System;

class Program
{
    static void Main()
    {
        var (a, b) = (1, 2);

        // M(string) の方が呼ばれる
        M($"{a}, {b}");

        // こう書けば M(FormattableString) の方
        M((FormattableString)$"{a}, {b}");
    }

    static void M(string x) => Console.WriteLine("string");
    static void M(FormattableString x) => Console.WriteLine("FormattableString");
}

同様に、ラムダ式は、デリゲート型で受け取る場合と式ツリーで受け取る場合で異なる挙動になります。 こちらは推論は効かず、オーバーロード解決もできなくなります。

using System;
using System.Linq.Expressions;

class Program
{
    static void Main()
    {
        M(x => x);
    }

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

ただし、次節で説明しますが、ラムダ式の型推論は結構優秀で、 ちゃんと推論が働きつつ、オーバーロード解決できる場合も多いです。

ラムダ式

ラムダ式の型推論は相当優秀で、結構複雑なオーバーロード解決もできたりします。 例えば、以下の M(x => x) はちゃんとコンパイルできます。

using System;

class Program
{
    static void Main()
    {
        // x の素通し = 引数と戻り値が一致 = Fucn<int, int> の方だけなのでそっちが選ばれる
        // x の型は int に
        M(x => x);

        // 明示的に double を返すと Func<int, double> の方が選ばれる
        // x の型は int に
        M(x => (double)x);

        // この場合、引数と戻り値が一致してるという条件では int なのか string なのか区別できなくてエラー
        N(x => x);
    }

    static void M(Func<int, int> x) => Console.WriteLine("int → int");
    static void M(Func<int, double> x) => Console.WriteLine("int → double");

    static void N(Func<int, int> x) => Console.WriteLine("int → int");
    static void N(Func<string, string> x) => Console.WriteLine("int → int");
}
Ver. 6.0

ちなみに、ラムダ式がらみの型推論/オーバーロード解決は、C# 6.0 で少し改良がありました。 以下のように、多段のラムダ式でちゃんとオーバーロード解決できるようになったのは C# 6.0 からです。 また、「匿名メソッド式はラムダ式と違って式ツリーにならない」という条件が加味されたのも C# 6.0 からです。

using System;
using System.Linq.Expressions;

class Program
{
    static void Main()
    {
        // M(() => { }) だと Action か Expression<Action> か区別つかないものの
        // 匿名メソッド式の場合は式ツリー化できない仕様なので、M(Action) で確定
        // なのに以前はこれもエラーになってた(C# 6.0 からは M(Action) が呼ばれる)
        M(delegate () { });

        // 以下のような、多段のラムダ式でちゃんとオーバーロード解決できるのは C# 6.0 から
        // Func<int, Func<int>> の方
        M(() => () => 1);
        // Func<int, Func<double>> の方
        M(() => () => 1.0);
    }

    // ラムダ式だと区別できないものの、匿名メソッド式なら Action で確定
    static void M(Actionx) => Console.WriteLine("Action");
    static void M(Expression<Action> x) => Console.WriteLine("Expression");

    // () => () => 1 みたいな、多段のラムダ式
    static void M(Func<Func<int>> x) => Console.WriteLine("() → () → int");
    static void M(Func<Func<double>> x) => Console.WriteLine("() → () → int");
}

更新履歴

ブログ