VB.NETでもクラスとモジュールの違いを知りたい

はじめに

皆さんは新人くんの

VBのクラスとモジュールの違いは何ですか?

の質問にどのように答えているでしょうか。

こんな時の対応は大体以下の感じになると思います。

  1. よくわからないから「ググれ」で済ませて軽蔑される
  2. 機能面での話をさらっとして尊敬される
  3. 機能面での話をすっとばして内部実装の細かい話をしてドン引きされる

正直、2.の機能面の話は死ぬほどいろんなところで書かれているはず*1なので、ここでは3.の方面でまとめてみようと思います。

仕様上のモジュール

過去の記事でVBの仕様書が見つからないと言ったな。アレは嘘だ。

というわけでVisual Studio 2017をインストールするとx64環境だと

C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VB\Specifications\1041

あたりに付いてくるっぽいです。しかも日本語で

今回の件から弊社が得る教訓は"SDKをインストールした際についてくるドキュメントも存外馬鹿にはできない"ということだ。

さて、仕様書からモジュールについて引用すると

"標準モジュール" は、メンバーが暗黙的にSharedである型です。標準モジュールのスコープは、標準モジュール宣言自体だけではなく、標準モジュールが含まれる名前空間の宣言空間まで及びます。標準モジュールはインスタンス化できません。標準モジュール型の変数を宣言すると、エラーになります。

標準モジュールのメンバーには 2 つの完全修飾名があります。1 つは標準モジュール名を持たず、もう 1 つは標準モジュール名を持ちます。名前空間内の複数の標準モジュールで、特定の名前の 1 つのメンバーを定義できます。そのため、いずれかのモジュールの外部でその名前を修飾せずに参照すると、あいまいになります。

~中略~

モジュールは名前空間内でのみ宣言でき、別の型の中で入れ子にすることはできません。標準モジュールはインターフェイスを実装できません。標準モジュールは暗黙的にObjectから派生し、Sharedコンストラクターだけを持ちます。

標準モジュールのメンバーは、メンバー宣言によって導入されたメンバーと、Objectから継承されたメンバーです。標準モジュールは、インスタンス コンストラクター以外の任意の型のメンバーを持つことができます。標準モジュール型のすべてのメンバーは、暗黙的にSharedになります。

通常、標準モジュールのメンバー宣言に指定できるのは、PublicFriend、またはPrivateのアクセスだけです。ただし、Objectを継承するメンバーをオーバーライドする場合は、Protectedおよび Protected Friendのアクセス修飾子も指定できます。標準モジュールのメンバー宣言にアクセス修飾子が含まれない場合は、既定でPublicアクセスが宣言されます。ただし、変数の場合、既定はPrivateアクセスです。

前述のとおり、標準モジュール メンバーのスコープは、標準モジュール宣言が含まれる宣言です。Objectを継承するメンバーはこの特殊なスコープに含まれません。これらのメンバーにはスコープがなく、常にモジュールの名前で修飾する必要があります。メンバーにFriendアクセスが指定されている場合、そのスコープは同じプログラムまたはFriendアクセスが指定されたアセンブリ内で宣言された名前空間メンバーだけに及びます。

"拡張メソッド" を使用すると、型宣言の外部からメソッドを型に追加できます。拡張メソッドとは、System.Runtime.CompilerServices.ExtensionAttribute属性が適用されているメソッドのことです。拡張メソッドは、標準モジュール内でだけ宣言でき、メソッドによって拡張される型を指定する 1 つ以上のパラメーターを持つ必要があります。

Microsoft(R)Visual Basic(R)言語仕様 - (C) 2012 Microsoft Corporation.All Rights Reserved.

だそうです。

実用上で注意しないといけないのは

  • モジュール内のメンバーはすべてSharedC#でいうstatic)になる
  • モジュール型の変数は宣言できない
  • 名前空間内でのみ宣言でき、入れ子で宣言できない
  • モジュールのメンバー宣言に指定できるのは、PublicFriendC#internal)、またはPrivate
  • 拡張メソッドはモジュールにしか宣言できない
  • 名前空間内で一意なメンバー名であればモジュール名を省略できる

といったところです。

ここまで説明出来て100点です。

モジュールの内部実装

表面上の使い方で満足しないのが我々エクストリームVBerです。

単純なコードのILを確認してみましょう。

なお、ここでは以下の環境で検証しています。

Public Module Hello
    Public Sub Hoge()
        Console.WriteLine("Hello")
    End Sub
End Module
.class public sealed auto ansi 
  ConsoleApp1.Hello
    extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute::.ctor() 
    = (01 00 00 00 )

  .method public static void 
    Hoge() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldstr        "Hello"
    IL_0006: call         void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop          
    IL_000c: ret          
  }
}

つまり、以下のコードとほぼ等価なことが分かります。

<Microsoft.VisualBasic.CompilerServices.StandardModule>
Public NotInheritable Class World
    Private Sub New()
        ' Nothing to do.
    End Sub

    Public Shared Sub Hoge()
        Console.WriteLine("World")
    End Sub
End Class
.class public sealed auto ansi 
  ConsoleApp1.World
    extends [mscorlib]System.Object
{
  .custom instance void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute::.ctor() 
    = (01 00 00 00 )

  .method private specialname rtspecialname instance void 
    .ctor() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldarg.0      // this
    IL_0002: call         instance void [mscorlib]System.Object::.ctor()
    IL_0007: nop          
    IL_0008: ret          
  }

  .method public static void 
    Hoge() cil managed 
  {
    .maxstack 8
    IL_0000: nop          
    IL_0001: ldstr        "World"
    IL_0006: call         void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret          
  }
}

デフォルトコンストラクタはさすがに封殺できないので、プライベートコンストラクタを明示的に宣言することでデフォルトコンストラクタを消し去っています。

StandardModuleAttributeがキモのようで、VBコンパイラはこの属性が付与されたクラスをモジュールとして認識しているようです。 そのため、C#のクラスにStandardModuleAttributeを付与するだけでVBから利用できないクラスを爆誕させることが出来ます。

f:id:jyuch:20170603001750p:plain

f:id:jyuch:20170603001808p:plain

余談ですが、同一ソリューション内であればVBコンパイラStandardModuleAttributeが付与されたクラスとモジュールを区別できるようです。 これがロスリンの力か・・・

おわりに

個人的な話でアレですが、弊社としては拡張メソッドを定義する以外ではモジュールは使いたくないですね。 オブジェクト指向に反するというのも若干ありますが、別に弊社はオブジェクト指向原理主義ではないのでそれについてはあまり気にしません。 やはり名前空間の汚染は正直気持ちのいいものではありませんし、レガシーVBの作法を持ち込まれてもねぇ・・・といった感じです。*2

おわり

*1:調べてはいない

*2:そもそもで言えば、VB.NETという言語自体がレガシーVBの作法を.NET Frameworkに持ち込むための言語なのですが