雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

再考・ nullable な引数を持つメソッドをオーバーロードする場合の解決

※ この記事の (1), (2), (3), (7) については解釈ミスがあったため追加記事を書きました。
気づいたらもう私の順番らしく。
って事で、本記事は C# Advent Calendar 2011 第 12 日目へのエントリー記事です。
……とは云え、ここのところちっとも C# 言語関連の記事書いてないし、正直、最近は Windows Phone の事位しかやってないので、何を書こうか悩む。。。
……で、とりあえず手近にネタが転がってないかなと思って過去の自分の記事を読み返していたら、↓この一連の記事を見つけました。

これ、もう 2 年以上前なんですね。
しかしよく見るとパラメータ配列に言及してなかったりするし、 C# 4.0 になって dynamic が追加されてたりするので、この機会に改めてオーバーロードの解決について一通りまとめてみたいと思います。
※各説明の最後にある項目番号は C# 4.0 言語仕様書の項目番号に対応します)

(1) int vs short

static void Main()
{
  Hoge(0);
}
static void Hoge(int a)
{
  Console.WriteLine("int");
}
static void Hoge(short a)
{
  Console.WriteLine("short");
}
// [output]
// int

初っ端から以前の議論から外れている例ですが。。。
引数として渡されている "0" は整数リテラルなので int と判断されます。 (2.4.4.2)
ここで、 int から short への暗黙的な型変換は存在しないため、 short 側のメソッドは適用対象メソッドに含まれません。 (7.5.3.1)
# 余談ですが、このケースで int 側のオーバーロードメソッドが存在しない場合は、引数の "0" が暗黙的に short へ型変換されて short 側のメソッドが呼ばれます。 (6.1.9)

(2) int vs int?

static void Main()
{
  Hoge(0);
}
static void Hoge(int a)
{
  Console.WriteLine("int");
}
static void Hoge(int? a)
{
  Console.WriteLine("int?");
}
// [output]
// int

0 は int にも int? にも変換可能ですが、 int から int? への暗黙的な型変換は存在する一方で int? から int への暗黙的な型変換は存在しないため、 int 側のメソッドが呼ばれます。 (6.1.4 / 6.5.3.5)

(3) int vs short?

static void Main()
{
  Hoge(0);
}
static void Hoge(int a)
{
  Console.WriteLine("int");
}
static void Hoge(short? a)
{
  Console.WriteLine("short?");
}
// [output]
// int

このケースも同様です。
int より short? の方がより限定的なので short? 側のメソッドが呼ばれます。 (6.5.3.5)
(訂正)
(1) と同様に int 側のメソッドが呼ばれます。
※初期アップ時の解釈をミスってました。結果含めて訂正します。

(4) int? vs short?

static void Main()
{
  Hoge(0);
}
static void Hoge(int? a)
{
  Console.WriteLine("int?");
}
static void Hoge(short? a)
{
  Console.WriteLine("short?");
}
// [output]
// short?

null 許容型は同じ型の null 非許容型と同等の暗黙変換を使用可能です。 (6.1.5)
従って、 (3) と同様により限定的な型である short 側のメソッドが呼ばれます。 (7.5.3.5)

(5) int? vs short?

static void Main()
{
  Hoge(null);
}
static void Hoge(int? a)
{
  Console.WriteLine("int?");
}
static void Hoge(short? a)
{
  Console.WriteLine("short?");
}
// [output]
// short?

null リテラルは任意の null 許容型への暗黙的な変換が存在します。 (6.1.4)
また、 (3) に書いた通り、 null 許容型は同じ型の null 非許容型と同等の暗黙変換を使用可能です。 (6.1.5)
そして、 int よりも short がより限定的な型なので short 側のメソッドが呼ばれます。 (7.5.3.5)

(6) int vs int? (拡張メソッドとして short を追加)

static void Main()
{
  var f = new Foo();
  f.Hoge(0);
}
public class Foo
{
  public void Hoge(int a)
  {
    Console.WriteLine("int");
  }
  public void Hoge(int? a)
  {
    Console.WriteLine("int?");
  }
}
public static class FooExtensions
{
  public static void Hoge(this Foo, short a)
  {
    Console.WriteLine("short");
  }
}
// [output]
// int

拡張メソッドが呼ばれるのは、対象クラスのメソッドに適用可能なメソッドがひとつもない場合に限られます。 (7.6.5.1)

(7) int vs dynamic

static void Main()
{
  Hoge(0);
}
static void Hoge(int a)
{
  Console.WriteLine("int");
}
static void Hoge(dynamic a)
{
  Console.WriteLine("dynamic");
}
// [output]
// int

dynamic 型は適用可能メソッドの検索に於いては object 型と同等に扱われます。 (3.6)
そして、 dynamic(object) よりも int がより限定的な型なので int 側のメソッドが呼ばれます。 (7.5.3.5)

(8) int? vs dynamic

static void Main()
{
  Hoge(null);
}
static void Hoge(int? a)
{
  Console.WriteLine("int?");
}
static void Hoge(dynamic a)
{
  Console.WriteLine("dynamic");
}
// [output]
// int?

(5) と (7) の組み合わせですね。

(9) int vs params int[]

static void Main()
{
  Hoge(0);
}
static void Hoge(int a)
{
  Console.WriteLine("int");
}
static void Hoge(params int[] a)
{
  Console.WriteLine("params int[]");
}
// [output]
// int

パラメータ配列に対して展開形式で適用するものより通常形式で適用可能なものの方がより適切になります。 (7.5.3.2)

(10) int? vs params int[]

static void Main()
{
  Hoge(null);
}
static void Hoge(int? a)
{
  Console.WriteLine("int?");
}
static void Hoge(params int[] a)
{
  Console.WriteLine("params int[]");
}
// ※このパターンはコンパイルエラーになります

null リテラルは任意の配列型への暗黙的な参照変換が存在します。 (6.1.6)
従って null リテラルに対してパラメータ配列は適用可能となります。 (7.3.5.1)
しかし、 int? と int[] の間でオーバーロードの解決を行う事が出来ないため、コンパイル時にエラーとなります。 (7.5.3.2)

(11) dynamic vs params int[]

static void Main()
{
  Hoge(null);
}
static void Hoge(dynamic a)
{
  Console.WriteLine("dynamic");
}
static void Hoge(params int[] a)
{
  Console.WriteLine("params int[]");
}
// [output]
// params int[]

(7) に書いた通り、 dynamic 型は適用可能メソッドの検索に於いては object 型と同等に扱われます。 (3.6)
また、 (10) に書いた通り、 null リテラルは任意の配列型への暗黙的な参照変換が存在するため、 null リテラルに対してパラメータ配列は適用可能となります。 (6.1.6 / 7.3.5.1)
そして、 int は dynamic への暗黙的な参照変換が存在します。 (6.1.6)
一方で dynamic(object) から int
への暗黙的な参照変換は存在しません。
従って dynamic(object) よりも int[] がより限定的な型と云えるのでパラメータ配列側のメソッドが呼ばれます。 (7.5.3.5)

(12) dynamic vs params dynamic[]

static void Main()
{
  Hoge(null);
}
static void Hoge(dynamic a)
{
  Console.WriteLine("dynamic");
}
static void Hoge(params dynamic[] a)
{
  Console.WriteLine("params dynamic[]");
}
// [output]
// params dynamic[]

(11) と同様で、 dynamic(object) よりも dynamic[] がより限定的な型と云えるのでパラメータ配列側のメソッドが呼ばれます。 (7.5.3.5)

まとめ

基本的には、「より限定的な型(暗黙的な変換が可能な型ペアの変換元側)が適用される」と覚えておけば十分かと思います。<こんだけ長々と書いておいてその結論かい
# まぁ、そもそも普段のプログラミングではこの辺を意識するケースはあまりない気もしますけど。 :p
なお、 C# 言語仕様書は MSDN から入手可能です。
Visual Studio ( Professional 以上)がインストールされている環境では、インストールディレクトリ配下の VC#/Specifications にインストール言語版の言語仕様書が格納されています。