概要
関数で説明しましたが、 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");
}