VB.NETでもクラスとモジュールの違いを知りたい
はじめに
皆さんは新人くんの
VBのクラスとモジュールの違いは何ですか?
の質問にどのように答えているでしょうか。
こんな時の対応は大体以下の感じになると思います。
- よくわからないから「ググれ」で済ませて軽蔑される
- 機能面での話をさらっとして尊敬される
- 機能面での話をすっとばして内部実装の細かい話をしてドン引きされる
正直、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
になります。通常、標準モジュールのメンバー宣言に指定できるのは、
Public
、Friend
、または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.
だそうです。
実用上で注意しないといけないのは
- モジュール内のメンバーはすべて
Shared
(C#でいうstatic
)になる - モジュール型の変数は宣言できない
- 名前空間内でのみ宣言でき、入れ子で宣言できない
- モジュールのメンバー宣言に指定できるのは、
Public
、Friend
(C#のinternal
)、またはPrivate
- 拡張メソッドはモジュールにしか宣言できない
- 名前空間内で一意なメンバー名であればモジュール名を省略できる
といったところです。
ここまで説明出来て100点です。
モジュールの内部実装
表面上の使い方で満足しないのが我々エクストリームVBerです。
単純なコードのILを確認してみましょう。
なお、ここでは以下の環境で検証しています。
- Windows10 Pro x64 1703
- Visual Studio 2017 Community
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から利用できないクラスを爆誕させることが出来ます。
余談ですが、同一ソリューション内であればVBコンパイラはStandardModuleAttribute
が付与されたクラスとモジュールを区別できるようです。
これがロスリンの力か・・・
おわりに
個人的な話でアレですが、弊社としては拡張メソッドを定義する以外ではモジュールは使いたくないですね。 オブジェクト指向に反するというのも若干ありますが、別に弊社はオブジェクト指向原理主義ではないのでそれについてはあまり気にしません。 やはり名前空間の汚染は正直気持ちのいいものではありませんし、レガシーVBの作法を持ち込まれてもねぇ・・・といった感じです。*2
おわり
*1:調べてはいない
*2:そもそもで言えば、VB.NETという言語自体がレガシーVBの作法を.NET Frameworkに持ち込むための言語なのですが
VB.NETとログについての伺か
はじめに
弊社はロギングライブラリとかの扱いについて割と長い間悩んでいたのですが、自分の中のとりあえずの答えが出たので今回はそんな感じです。
ログライブラリへの強依存
さて、弊社はログライブラリとしてNLogに慣れ親しんでいるのでサンプル中で使うライブラリとしてNLogを使用しますが、Serilogやlog4netを使用しても大して変わらないと思います。たぶん*1
さて、NLogでログを取得するコードを書くとすると以下のような感じになるのではないでしょうか。
Module Module1 Sub Main() Dim model = New SomeModel() Console.WriteLine($"{model.Add(1, 2)}") Console.WriteLine($"{model.Subtract(2, 1)}") Console.WriteLine($"{model.Subtract(1, 2)}") End Sub End Module Class SomeModel Private Shared ReadOnly Logger As NLog.ILogger = NLog.LogManager.GetCurrentClassLogger() Function Add(x As Integer, y As Integer) As Integer Dim result = x + y Logger.Info("{0} + {1} = {2}", x, y, result) Return result End Function Function Subtract(x As Integer, y As Integer) As Integer Dim result = x - y If result < 0 Then Logger.Warn("{0} - {1} is negative ({2})", x, y, result) Return 0 Else Logger.Info("{0} - {1} = {2}", x, y, result) Return result End If End Function End Class
はい、そうですね。べっとりNLogに依存しちゃってます。 仮に他のロギングライブラリに移行することになった場合、ロギングコードを修正する必要が出てきますし、細かくログを取得しようとしていた場合には修正箇所は相当な数になるはずです。
インタフェース化とIoCコンテナ
はいはいインターフェースインターフェース。 メソッドをインターフェースに切り出すことができればあとはIoCコンテナとかを使って外部からロガーを注入してみんなハッピーになれます。
と、ここで壁にぶち当たりました。
どのようなメソッドとしてインタフェースを切り出しましょう
NLogやSerilog、log4netではログの書き出しメソッドはログレベルとして実装されています。 まずはログレベルで検討してみましょう。
NLog | Serilog | log4net | 簡単な説明 |
---|---|---|---|
Trace | Verbose | 該当なし | めっちゃ細かい |
Debug | Debug | Debug | デバッグ情報 |
Info | Information | Info | 通常のイベント情報 |
Warn | Warning | Warn | クリティカルではない警告 |
Error | Error | Error | エラー |
Fatal | Fatal | Fatal | ちょー致命的 |
という感じで多少であるものの、各ロギングライブラリで使用できるログレベルには差異があります。
また、NLogとlog4netはメッセージを出力するタイプのログライブラリですが、Serilogは構造化ログライブラリなのでインターフェースでメソッドのシグネチャを統一しても幸せになれそうではありません。
ではどのようなインタフェースを用意すればいいのかと言いますと、ログイベントを意味論としてメソッドを用意してあげれば良いのではとの結論に至りました。
先ほどの例ですと、以下のような感じになります。
Interface IMyLogger Sub CalcAdd(x As Integer, y As Integer, result As Integer) Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) End Interface
この方法ですとプロジェクトごとにインターフェースが異なりますが、そもそもログイベントもプロジェクトごとに異なるんだからそもそもプロジェクトにまたがって共通化する必要はありませんよね。
また、イベントの種類だけメソッドが増大するという問題もありますが、そちらは、まぁ、その、いい感じにお願いします。
そんな感じで、Simple Injectorを使いつつ書き直したのが以下になります。
Imports NLog Imports SimpleInjector Module Module1 Sub Main() Dim c = New Container() Dim logger = New MyLogger(LogManager.GetCurrentClassLogger()) c.RegisterSingleton(Of IMyLogger)(logger) c.Register(Of SomeModel)() c.Verify() Dim model = c.GetInstance(Of SomeModel)() Console.WriteLine($"{model.Add(1, 2)}") Console.WriteLine($"{model.Subtract(2, 1)}") Console.WriteLine($"{model.Subtract(1, 2)}") End Sub End Module Class SomeModel Private ReadOnly _logger As IMyLogger Sub New(logger As IMyLogger) _logger = logger End Sub Function Add(x As Integer, y As Integer) As Integer Dim result = x + y _logger.CalcAdd(x, y, result) Return result End Function Function Subtract(x As Integer, y As Integer) As Integer Dim result = x - y If result < 0 Then _logger.SubtractResultIsNegative(x, y, result) Return 0 Else _logger.CalcSubtract(x, y, result) Return result End If End Function End Class Interface IMyLogger Sub CalcAdd(x As Integer, y As Integer, result As Integer) Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) End Interface Class MyLogger Implements IMyLogger Private ReadOnly _nlog As ILogger Public Sub New(logger As ILogger) _nlog = logger End Sub Public Sub CalcAdd(x As Integer, y As Integer, result As Integer) Implements IMyLogger.CalcAdd _nlog.Info("{0} + {1} = {2}", x, y, result) End Sub Public Sub CalcSubtract(x As Integer, y As Integer, result As Integer) Implements IMyLogger.CalcSubtract _nlog.Info("{0} - {1} = {2}", x, y, result) End Sub Public Sub SubtractResultIsNegative(x As Integer, y As Integer, result As Integer) Implements IMyLogger.SubtractResultIsNegative _nlog.Warn("{0} - {1} is negative ({2})", x, y, result) End Sub End Class
この辺のアイデアは弊社が独自で思いついたというよりはSLABの概念とかLightNodeのコードをパクった参考にしたところがあるのでオリジナルへのリンクを付け加えておきます。
余談(ってほど余談でもない)
NLogの${callsite}
とかのスタックフレームのスキップ
<target xsi:type="ColoredConsole" name="console" layout="${longdate} ${uppercase:${level}} ${message}${newline}${stacktrace}${newline}${callsite}" />
みたいなレイアウトを設定している場合、出力結果は
2017-05-06 01:26:54.5080 INFO 1 + 2 = 3 Module1.Main => SomeModel.Add => MyLogger.CalcAdd ConsoleApp2.MyLogger.CalcAdd 3 2017-05-06 01:26:54.5281 INFO 2 - 1 = 1 Module1.Main => SomeModel.Subtract => MyLogger.CalcSubtract ConsoleApp2.MyLogger.CalcSubtract 1 2017-05-06 01:26:54.5281 WARN 1 - 2 is negative (-1) Module1.Main => SomeModel.Subtract => MyLogger.SubtractResultIsNegative ConsoleApp2.MyLogger.SubtractResultIsNegative 0
みたいになって、ログの実装クラスがしゃしゃり出てきてもんにょりします。
そんな時はskipFrames=Integer
を設定してあげるといい感じになります。
<target xsi:type="ColoredConsole" name="console" layout="${longdate} ${uppercase:${level}} ${message}${newline}${stacktrace:skipFrames=1}${newline}${callsite:skipFrames=1}" />
2017-05-06 01:30:57.3469 INFO 1 + 2 = 3 Module1.Main => SomeModel.Add ConsoleApp2.SomeModel.Add 3 2017-05-06 01:30:57.3700 INFO 2 - 1 = 1 Module1.Main => SomeModel.Subtract ConsoleApp2.SomeModel.Subtract 1 2017-05-06 01:30:57.3700 WARN 1 - 2 is negative (-1) Module1.Main => SomeModel.Subtract ConsoleApp2.SomeModel.Subtract 0
IoCコンテナとサービスロケーター
IoCコンテナを使い始めると
コンテナをコンストラクタ引数で渡して、コンテナから必要なオブジェクトを取り出せばいいんだ
みたいに勘違いしやすいと思います。というよりは弊社は勘違いしていました。
今回ので例を示すとこんな感じですね。
Class SomeModel Private ReadOnly _logger As IMyLogger Sub New(container As Container) _logger = container.GetInstance(Of IMyLogger)() End Sub Function Add(x As Integer, y As Integer) As Integer '... End Function Function Subtract(x As Integer, y As Integer) As Integer '... End Function End Class
これには以下のデメリットが発生します。
- 本来
IMyLogger
だけに依存したいはずなのに、IoCコンテナ(Container
)にも依存してしまっている- 本来なら必要のない別のクラスに依存することになり、依存を減らしたいはずなのに逆に増えてしまっている
- そのクラスが依存しているインターフェース・クラスがコンストラクタのシグネチャから判別がつかない
- コンテナが保持するインターフェース・クラスをすべて利用することが出来てしまうため、不用意に不要なクラスへアクセス出来てしまうため、なんかやばい
なんで、まぁサービスロケーターとして使うのはやめましょうねって感じで。
おわりに
弊社は最近では自動化ツールを作ったりコンソールアプリケーションを作成する機会が多いです。
そんな中、どのみち必要になるから最初からNuGetでぶち込んでおけと思うのが以下の3つです。
今回のでロギングライブラリのもんにょりが消えたので、残るはコマンドラインオプションパーサーになるのですが、今のところしっくりくるのがありません。
なければ作るしかないっぽいのでアレですが、当面はいい感じのを探していこうと思っています。
おわり
VB.NETでもAutofacで依存性を注入したい
はじめに
OpenCoverのコードを読んでいたら、AutofacというDIライブラリが使用されており、気になったのでそれについてです。
チュートリアル
Getting Started — Autofac 4.0 documentation
大体の手順をざっくりと列挙すると
- 脳内でいい感じに制御の反転を組み立てる
Autofac
の参照を追加する- アプリケーションの開始時に
ContainerBuilder
のインスタンスを生成し- コンポーネントを登録して
- コンテナを生成して
- アプリケーションの実行の間
- ライフタイムスコープを生成して
- ライフタイムスコープを使ってコンポーネントのインスタンスを解決する
といった感じです。
Imports Autofac Module Module1 Sub Main() Dim builder = New ContainerBuilder() builder.RegisterType(Of ConsoleOutput)().As(Of IOutput)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() Dim out = scope.Resolve(Of IOutput)() out.WriteLine("hello world") End Using End Sub End Module Interface IOutput Sub WriteLine(value As String) End Interface Class ConsoleOutput Implements IOutput, IDisposable Public Sub WriteLine(value As String) Implements IOutput.WriteLine Console.WriteLine(value) End Sub Public Sub Dispose() Implements IDisposable.Dispose Console.WriteLine("IDisposable.Dispose") End Sub End Class
hello world IDisposable.Dispose
直接コンテナからコンポーネントのインスタンスを生成せずに、ライフタイムスコープからインスタンスを生成することでライフタイムスコープが破棄された時点でライフタイムスコープから生成されたインスタンスも同時に破棄されます。
また、コンポーネントに複数のコンストラクタが存在する場合、コンテナ内に引数となりうる他のコンポーネントが登録されていればそのコンストラクタを使用してインスタンスを生成してくれます。
Module Module1 Sub Main() With "ジャムおじさんとバタコ" Dim builder = New ContainerBuilder() builder.RegisterType(Of AnpanMan)().As(Of IAnpanMan)() builder.RegisterType(Of Jam)().As(Of IJam)() builder.RegisterType(Of Batako)().As(Of IBatako)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() scope.Resolve(Of IAnpanMan)().Anpanchi() End Using End With With "バタコのみ" Dim builder = New ContainerBuilder() builder.RegisterType(Of AnpanMan)().As(Of IAnpanMan)() builder.RegisterType(Of Batako)().As(Of IBatako)() Dim container = builder.Build() Using scope = container.BeginLifetimeScope() scope.Resolve(Of IAnpanMan)().Anpanchi() End Using End With End Sub End Module Interface IBatako End Interface Class Batako Implements IBatako End Class Interface IJam End Interface Class Jam Implements IJam End Class Interface IAnpanMan Sub Anpanchi() End Interface Class AnpanMan Implements IAnpanMan Private _anpanchi As String Public Sub New() _anpanchi = "顔が濡れて力が出ない" End Sub Public Sub New(batako As IBatako, jam As IJam) _anpanchi = "元気100倍アンパンマン" End Sub Public Sub Anpanchi() Implements IAnpanMan.Anpanchi Console.WriteLine(_anpanchi) End Sub End Class
元気100倍アンパンマン 顔が濡れて力が出ない
おわりに
無事に元気100倍アンパンチが出来ました。
余談ですが、ジャムおじさんやバタコさんは人間じゃなかったんですね。
おわり