拡張メソッド != 静的メソッド

C#3.0 の拡張メソッドの怖いお話。拡張メソッドと静的メソッドは構文の字面だけの違いで、ほかは同じだよと言われることが多いけど、実は違います。仕様書に書いてはあるけど、はっきり注意を喚起していないので。拡張メソッドの仕様はこちら
念のため引用。「拡張メソッドは、インスタンス メソッドに比べて、見つけにくく、機能も限られています。このため、拡張メソッドの使用はできるだけ控え、インスタンス メソッドが適していない場合や使用できない場合にのみ使用することをお勧めします。」拡張メソッドは飛び道具だから振り回さないほうがイイですね。

関連ネタ : id:siokoshou:20070901#p2, id:siokoshou:20070903#p1
via http://blogs.msdn.com/sreekarc/archive/2007/10/11/consequences-of-conversion-rules-for-instance-parameters.aspx
Sreekar さんの説明よりずっと詳しく書きました。コンパイラチームに負けないぜぃw

using System;

class Program
{
  static void Main()
  {
    0.Foo();       // 1
    0L.Foo();
    Console.WriteLine();

    Ext.Foo( 0 );  // 2
    Ext.Foo( 0L );
    Console.ReadKey();
  }
}

static class Ext
{
  public static void Foo( this long x ) { Console.WriteLine( "long" ); }
  //public static void Foo( this int x ) { Console.WriteLine( "int" ); }
  public static void Foo( this object x ) { Console.WriteLine( "Object" ); }
  public static void Foo( this short x ) { Console.WriteLine( "short" ); }
  //public static void Foo( this sbyte x ) { Console.WriteLine( "sbyte" ); }
}

このコードで、1 と 2 はどれが呼ばれると思いますか?クイズにしても誰も正解しなそうなので自粛(^^;
1 は Object、2 は short が呼ばれます。インスタンスメソッド構文で呼んだ時と、静的メソッドとして呼んだときで違うメソッドが呼ばれています!イヤらしいですねぇ、何考えて設計したんでしょう。不用意な結びつきを避けたのかもしれません。(追記:この部分はこれまで意識していなかったインスタンスメソッドと静的メソッドの違いが拡張メソッドで意識できるようになったため、あらためて気付いたってだけのことかもしれません。id:siokoshou:20071121#p2 参照)

これは変換規則の違いによるものだそうで、拡張メソッドの1つめのパラメータの変換規則が従来と違うことが原因でこうなるそうです。上で挙げた仕様書の「1 つ目の引数から 1 つ目のパラメータへの暗黙的な、ID 変換、参照の変換、またはボックス化変換が存在しないメソッドをすべて除外します。」の部分でさらっと触れています。
ID 変換というのは、従来、恒等変換と訳されていたもの。変換しない変換規則です。参照の変換とボックス化変換はわかりますよね(詳細は別として(^^;)。
で、何が言いたいのかというと、「暗黙の数値変換」をはじめいくつかの暗黙の変換がなくなっていますよ、と。

Sreekar さんの blog では、これは暗黙の数値変換がないためと説明してますが、仕様をよく読むと、暗黙の定数式変換がないせいの誤りではないかと思われます(^^; ちょっと余談ですが、2 が long じゃなくて short なのは暗黙の数値変換規則ではなく、暗黙の定数式変換ルールが使われたためです。sbyte を取る Foo() のコメントを外すとこっちが呼ばれます。

以下、いくつか消えた変換規則のサンプルを。これを C# 仕様書に追記してもらいたいな。

using System;

class Program
{
  static void Main()
  {
    0.Foo();        // Object
    Ext.Foo( 0 );   // short 暗黙の定数式変換
    Console.WriteLine();

    B b = new B { Val = 1 };
    b.Foo();         // Object
    Ext.Foo( b );    // A
    Console.WriteLine();

    bool n = true;
    n.Foo();         // Object
    Ext.Foo( n );    // bool?
    Console.WriteLine();

    int m = 5;
    m.Foo();         // Object
    Ext.Foo( m );    // long 暗黙の数値変換
    Console.ReadKey();
  }
}

static class Ext
{
  public static void Foo( this long x ) { Console.WriteLine( "long" ); }
  //public static void Foo( this int x ) { Console.WriteLine( "int" ); }
  public static void Foo( this object x ) { Console.WriteLine( "Object" ); }
  public static void Foo( this short x ) { Console.WriteLine( "short" ); }
  //public static void Foo( this sbyte x ) { Console.WriteLine( "sbyte" ); }

  public static void Foo( this A x ) { Console.WriteLine( "A" ); }
  //public static void Foo( this B x ) { Console.WriteLine( "B" ); }

  public static void Foo( this bool? x ) { Console.WriteLine( "bool?" ); }
}

class A { public int Val; }

class B
{
  public int Val;

  public static implicit operator A( B b )
  {
    return new A { Val = b.Val };
  }
}