他言語転向組が知るべきVB.NETのこと

初めに

VBという言語に触れてから暫くたったのですがVBJavaC#Pythonなどの言語とはかなり違った作法を持ち、その辺に苦しめられたのでそれに関するメモ的な何かです。

結論から言うと、VBが不愉快な挙動をしたら速攻でildasm*1を起動してアセンブリを確認してf○ckと叫んだ後対策を考えると言う流れを辿れば大体はいい感じになんとかなります。と言うよりは必然的にそうなります。

VB.NET全体的な事

おそらくVBプログラマとその他の言語のプログラマで良し悪しの意見が分かれると思いますが、VBコンパイル時にかなりコードに介入を行います。

私もVBの言語仕様を完全に把握している訳では無いのですが、以下のVBの言語機能を達成するためにコードに介入します。 この介入が他の言語からの転向組からするとかなり違和感を感じます。

他にも、おそらくレガシーVBとの見た目的な互換性を保つためと思われるものとして以下のVBの仕組みがあります。

  • コンパイラにとって曖昧でない限りモジュール名を省略してメソッドやフィールドにアクセスできる仕様
  • レガシーVBのバリアント型を意識したであろうSystem.Objectを引数に取るMicrosoft.VisualBasic名前空間以下のメソッド

名前空間による恩恵を全て闇に葬りかねないコンパイラにとって曖昧でない限り省略可能なモジュール名や、コンパイル時の型チェックをなかったことにしてくれる暗黙の型変換などは、正直私が初めてVBに触れた時に驚愕した内容でもあります。

暗黙の型変換

Option Strict Offが指定されたVBコンパイラの前にはもはや『数値』と『文字列で表現された数値』の違いなどありません。 仮に整数を引数に取る関数に文字列が渡されたところでVBコンパイラはしれっと文字列を数値に変換する操作をコードに挿入し変換された値をメソッドに渡します。 また、同様の操作がCharDataTimeなどにも適用されます。

例として、以下のコードが許容されます。

Sub Main()
    PrintInt("1")
End Sub

Sub PrintInt(a As Integer)
    Console.WriteLine(a)
End Sub

参考までに生成されたILがこちら。しれっと変換メソッドが呼ばれているのが分かります。 コンテキストによって値の型が変わる(様に見える)のはPerlに似ていますね。

IL_0000:  nop
IL_0001:  ldstr      "1"
IL_0006:  call       int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToInteger(string)
IL_000b:  call       void DiffOther.Module1::PrintInt(int32)
IL_0010:  nop
IL_0011:  nop
IL_0012:  ret

遅延バインディング

Option Strict Offが指定されている状態では、Object型の参照に格納されているインスタンスに対して任意のメソッド、フィールド及びプロパティの呼び出しが許容されます。そして実行時に参照先のインスタンスに指定された操作がある場合はその操作が呼び出されます。

あまり突っ込んだところまで見たわけではないのですが、リフレクションを用いているっぽいので事前バインディングよりも遅いです。 まぁ、遅いつっても一回二回程度なら人間が知覚できるほど程遅いわけではないのでよほどクリティカルなシステムを作らない限り心配しなくていいです。 つーかそんなクリティカルなシステムをVBで作る方が間違っているとも言えます。

ただ、単独ならともかくループ内で呼び出すとなるとになると話が変わります。やばいです。

Module Module1

    Sub Main()
        Dim hoge As Object = New Hoge()

        Console.WriteLine(hoge.Field1)
        Console.WriteLine(hoge.Property2)
        hoge.Method3(3)

        ' 非公開メソッドは呼び出せない。残念
        ' Hoge.Method4(4)
    End Sub

End Module

Class Hoge
    Public Field1 As Integer

    Public Property Property2 As Integer

    Public Sub New()
        Me.Field1 = 1
        Me.Property2 = 2
    End Sub

    Public Sub Method3(a As Integer)
        Console.WriteLine(a)
    End Sub

    Private Sub Method4(a As Integer)
        Console.WriteLine(a)
    End Sub
End Class

ローカル変数の暗黙的宣言

Option Explicit Offが指定されている場合は変数は暗黙的に宣言されます。 つまり、変数を宣言しなくても使えます。

ちなみに代入する前に変数の中身を参照することもできます。 つまるところOption Explicit Offを指定すると変数名のスペルミス一つ許されない殺伐としたコーディング環境が手に入れられます。

Sub Main()
    a = 20
    Console.WriteLine(20)
End Sub

上記のコードではObject型の変数aが宣言された後、20がボックス化されてaに格納されます。

Stringにおける等値演算子

コンパイラオプションに関係なく、"" = NothingTrueを返します。

答えは簡単。VBは文字列の比較にSystem.String演算子オーバーロードされた=ではなくMicrosoft.VisualBasic.CompilerServices.Operators::CompareStringを呼び出しているから。 なんでこいつを呼び出しているかというと、恐らくOption Compareによる文字列比較の挙動切り替えを実現するため。

なのでC#と同じ感覚でVBで文字列を比較すると落とし穴にはまるかも。

Sub Main()
    Dim result As Boolean
    result = "" = Nothing
    Console.WriteLine(result) ' Display True
End Sub

省略可能なモジュール名

VBにはモジュールというC#の静的クラスに一見似てるけど同一視すると爆死する要素があります。 モジュール内の要素を呼び出すとき、要素が曖昧でない限りモジュール名が省略できます。

つまり、以下のことができます。

Module Module1
    Sub Main()
        Display(1)
    End Sub
End Module

Module Hoge1
    Sub Display(a As Integer)
        Console.WriteLine(a)
    End Sub
End Module

何が悪いのと思われるかもしれませんが、仮に他のプログラマが知らずに別のモジュール内に同一の名前のメソッドを宣言したらどうでしょう。

Module Module1
    Sub Main()
        ' 'Display' は、モジュール 'DiffOther.Hoge1' および 'DiffOther.Hoge2' 内の宣言間においてあいまいです。
        Display(1)
    End Sub
End Module

Module Hoge1
    Sub Display(a As Integer)
        Console.WriteLine(a)
    End Sub
End Module

Module Hoge2
    Sub Display(a As Integer)
        Console.WriteLine(a)
    End Sub
End Module

モジュールを追加したら、エラーでコンパイルが通らなくなりました。解決策は

  • 曖昧にならないよう全てのDisplayメソッドの呼び出しにモジュール名を付与する
  • 別の名前にメソッド名を変える

がありますが、名前空間という概念を導入しているのにもかかわらずメソッド名が衝突してどちらかが名前を変えるというのはいささか不愉快な気分になります。あらかじめモジュールのメソッドの呼び出しでモジュール名を省略できる場合でもモジュール名を書くという解決策もありますが、すでに大量のコードが存在する場合は修正するのに手間がかかりすぎるという欠点もあります。

バリアント型を意識したメソッド

こちらは文法というよりはVBという言語の設計思想的な方面だと思いますが、Lenの様にオーバーロードしまくった挙句System.Objectを引数にとるメソッドやそもそもSystem.Objectしか引数として取らないメソッドがあります。

私はレガシーVBを実際に書いたことがないので詳しいことは分からないのですが、この辺はレガシーVBのバリアント型のような働きをSystem.Objectにもしてもらいたかった為と勝手に考えています。

まとめ

VB.NETは恐らくレガシーVBを書いていた人間が無理なく転向できるようにこのような仕組みを実装しているのだと思いますが、他の言語をやっていた身から言わせてもらうと例外的な物が多く直感的に分かりにくいと感じます。

逆に言うとその辺を理解してしまえばC#と同じように書けます。 それなら初めからC#で書けよというお話ですがそうは問屋がおろさないのが現実です。 世知辛いね。

おわり