LoginSignup
8
9

More than 5 years have passed since last update.

Visual BasicでSystem.Objectに対する拡張メソッドを実行すると失敗する(追記有り)

Last updated at Posted at 2015-11-04

どーいうことか?

検証とかするとき、Consoleに楽して出力できるように、以下のようなとても単純な拡張メソッドを作成して割と便利にC#では使っていた。1


    public static class DumpExtension
    {
        public static void Dump(this object data) => Console.WriteLine(data?.ToString() ?? "NULL");
    }

ちなみに、Visual Basicでは以下のようになる。2


Public Module DumpExtension
    <Extension()>
    Public Sub Dump(data As Object)
        Console.WriteLine(If((If((data IsNot Nothing), data.ToString(), Nothing)), "NULL"))
    End Sub
End Module

C#で扱ってる分には、割とよく動いたけど、故あって、Visual Basicで動かそうとしたら、うまく行かなかったことの顛末記と言うことで一つおつきあい頂ければ幸い。 3

どう失敗したのか?

Visual Basicで以下のようなサンプルを実行させようとしたら、


Module Module1

    Sub Main()
        dim refObj as Object="hello"
        dim valObj as Object=42

        Dim int As Integer =84
        Dim str As String="hello"

        'こいつらはうまくいく
        int.Dump()
        str.Dump()

        'System.MissingMemberExceptionが発生して失敗する。
        refObj.Dump()
        valObj.Dump()

    End Sub

End Module

さて、このとき、System.Object型以外の型は拡張メソッドを呼び出して想定通りの動きをしてくれる反面、困ったことにSystem.Object型はSystem.MissingMemberExceptionが発生してしまい、想定通りの動きをしてくれず失敗する。

なぜ失敗したのか?

失敗した理由は、System.Object型に対する遅延バインディングがVisual Basicでは発生するため。
他方、C#では、System.Object型に対する遅延バインディングは発生しない。

なぜコレがマズいかというと、コンパイル時に、拡張メソッドを呼び出す為のコード生成が行われず、遅延バインドを実行するためのコードが生成される。んで、遅延バインドを実行するコードはサンプルコード内のrefObjであれば、System.String型にvalObjであれば、System.Int32型にDump()なるメソッドが存在するという仮定の下に実行される。当然んなモンはないので、実行時にSystem.MissingMemberExceptionが発生しちまうというオチ。4

逃げちゃ駄目カナ?

遅延バインドってコトはSystem.Objectの参照先にDump()メソッドが存在すりゃ呼び出すわけで、以下のサンプルコードは成功しちゃう


Class Sample
    Public Property Value As Integer=100

    Public Sub Dump()
        Console.WriteLine("Sample.Dump() is called.")
        Console.WriteLine(Value.ToString())
    End Sub

    Public Overrides Function ToString() As String
        Console.WriteLine("ToString is called.")
        Return Value.ToString()
    End Function
End Class



Module Module1

    Sub Main()
        Dim sampleObj as Object=new Sample()

        '拡張メソッドでは無く自前のDumpが呼ばれるので、ToString()経由じゃないから、
        'Sample.Dump() is called.
        '100
        'と表示される。
        sampleObj.Dump()


    End Sub

End Module

この場合、SampleクラスのインスタンスメソッドであるDumpが遅延バインドの結果呼ばれるので、コンソールには、

Sample.Dump() is called.
100

と表示されることになる。

で、だこの先頭抱えることになるけど、C#で似た様なコトすると、当然のことながら、拡張メソッドのDumpメソッドの方が呼ばれる。


using System;

namespace ConsoleApplicationCS
{
    public static class DumpExtensions
    {
        public static void Dump(this object data) => Console.WriteLine(data?.ToString() ?? "NULL");
    }

    public class Sample
    {
        public int Value { get; } = 100;

        public void Dump()
        {
            Console.WriteLine("Sample.Dump() is called.");
            Console.WriteLine(Value);
        }

        public override string ToString()
        {
            Console.WriteLine("ToString() is called.");
            return Value.ToString();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            object hoge = new Sample();
            hoge.Dump();
        }


    }
}

上記サンプルの実行結果は、

ToString() is called.
100

となり、ほぼ同じことしてるのに呼び出しの結果がまるで違うことになる。
これは、コンパイル時にSystem.Object型に対するDump拡張メソッドに対する呼び出しが解決されるからってお話。

どっちも動くけど、呼び出し先が変化するとかホントやめて頂きたい所存。。。

わけがわからないよ

さて、気になって、Visual Basic Language Specifiction Version11.0の、11.3遅延バインディング式をつらつらと閲覧していたら、気になる記載があった。

コンパイル環境または Option Strict で厳密な型指定規則が指定されている場合、遅延バインディングによりコンパイル時のエラーが発生します。

非常にシンプルだが、ちょっと待てよと。。。
VisualStudioなら、プロパティを呼び出して、コンパイルタブを選択すると、Option Strictの選択コンボボックスから指定可能であり、デフォルトではOffとなっている。

さて、このオプションをOnにして、拡張メソッドが存在する状態でコンパイルする場合、どーなるのか試してみた。


Imports System.Runtime.CompilerServices

Public Module DumpExtension
    <Extension()>
    Public Sub Dump(data As Object)
        Console.WriteLine(If((If((data IsNot Nothing), data.ToString(), Nothing)), "NULL"))
    End Sub
End Module

Module Module1

    Sub Main()
        dim int as Integer =10
        Dim str as String ="hello"
        Dim now as DateTimeOffset=DateTime.Now


        int.Dump()
        str.Dump()
        now.Dump()

    End Sub

End Module

上記の場合、ちゃんとDumpメソッドが呼ばれて万事万端うまく行く。
で、わけがわからないのが以下


Imports System.Runtime.CompilerServices

Public Module DumpExtension
    <Extension()>
    Public Sub Dump(data As Object)
        Console.WriteLine(If((If((data IsNot Nothing), data.ToString(), Nothing)), "NULL"))
    End Sub
End Module

Module Module1

    Sub Main()
        dim int as Object =10
        Dim str as Object ="hello"
        Dim now as Object=DateTime.Now


        int.Dump()
        str.Dump()
        now.Dump()

    End Sub

End Module

当初、遅延バインディングが無効化されたので、もしかしたら、拡張メソッドの呼び出しを生成してくれるかも知れないと甘い期待を持ったけど、やっぱり甘い期待だった。
上記のように、Object型の変数に代入した場合、Dumpメソッドの呼び出しは、BC30574が発生して、ことごとくコンパイルすら通せなくなる。
コレは、先の遅延バインディングの仕様そのものの動作ではある反面、何か釈然としないのも又事実だったりする。5

C#でも発生する類似例

実は、C#でも類似例は発生する。
よく似たもの有るよね?遅延バインドしてくれるやつ。


using System;

namespace ConsoleApplicationCS
{
    public static class DumpExtension
    {
        public static void Dump(this object data) => Console.WriteLine(data?.ToString() ?? "NULL");
    }

    class Program
    {
        static void Main(string[] args)
        {
            dynamic integer = 42;
            dynamic text = "hello";

            integer.Dump();
            text.Dump();

        }


    }
}

なので、上記のサンプルは例外が発生して失敗する。
けど、Visual Basicでは、System.MissingMemberExceptionだったけど、C#では、Microsoft.CSharp.RuntimeBinder.RuntimeBinderExceptionと、発生する例外が異なるのでその点は要注意。

また、以下のサンプルは遅延バンディングに成功する。


using System;

namespace ConsoleApplicationCS
{
    public static class DumpExtension
    {
        public static void Dump(this object data) => Console.WriteLine(data?.ToString() ?? "NULL");
    }

    public class Sample
    {
        public int Value { get; } = 100;

        public void Dump()
        {
            Console.WriteLine("Sample.Dump() is called.");
            Console.WriteLine(Value);
        }

        public override string ToString()
        {
            Console.WriteLine("ToString() is called.");
            return Value.ToString();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            dynamic sample=new Sample();

            sample.Dump();

        }


    }
}

この場合、Visual Basicの遅延バインディングと同様に、Sample.Dump()が呼ばれるので、結果は

Sample.Dump() is called.
100

となる。

まとめ

最初、相当面食らったけど、色々と考えれば、確かにそだね~とまぁ、納得はできないまでも理解はできた。
以下は個人論だけど、C#はobjectdynamicのセマンティクスを分離してる反面、Visual Basicはその歴史的経緯6から
Objectがどっちのセマンティクスも持ってしまってる。
その辺の差異がイヤな方向に顕在化しちゃった例じゃないかなと。

最後に、今回は結果に対する検証であり、
System.Objectに対して拡張メソッドを提供すべきか否かという議論とは分離して頂ければ重畳。


  1. 個人的には、検証目的で有れば別段System.Objectに対する拡張メソッドの提供にそれほど抵抗はなかったりする。 

  2. IL Spyのデコンパイル結果なので、色々とアレゲです。諸々ご指摘ください。 

  3. 尚、筆者はC#perなので、Visual Basic側のサンプルに不備等有ればご指摘ください。 

  4. 拡張メソッドはコンパイル時に解決される。 

  5. 拡張メソッド界隈の仕様確認はまだなので、時間があれば加筆していきたい。 

  6. variant型とか、variant型とか、variant型とか・・・うっ頭が。。。 

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9