VBプログラマが知るべき9のこと by @masaru_b_cl

インスパイアードバイ:Javaプログラマが知るべき9のこと - @katzchang.contexts

最初はC#について書こうと思ったんだけど、大体一緒になっちゃうから、VBにしてみた。
なお、この記事に書いてあることは目新しいことでもなんでもなく、これまで各所でいろいろといわれていたことをまとめたものです。

Option Strict を On に

まず真っ先にしてほしいのが、「Option Strict」を「On」にすることである。
Option Strict ステートメント (Visual Basic) | Microsoft Docs

暗黙的なデータ型変換を拡大変換だけに制限します。

Option Strictは明示的にOnにしないと、既定ではOffとみなされるため、次のようなコードが書けてしまう。

Dim str As String
str = 100

上記のコードでは変数の宣言と値の設定が近い位置にあるため、文字列を扱っていることがすぐにわかるが、実際現場で目にするような長大なコード*1では、その変数が文字列を扱うのか数値を扱うのか即座に判別することができないため、保守が苦痛になる。

また、次のようなコードも書けてしまう。

Dim str As String
str = "12" + 34

Console.WriteLine(str) // ここの結果は?

Console.WriteLineでコンソールにどんな値が表示されるか、すぐにわかるであろうか?*2

Option StrictをOnにすることで、上記のようなコードは全てコンパイルエラーとなる。

では、どうやってOption Strictを設定するかというと、私はVisual Studioのオプションで、[Visual Basicの既定値]を変更してしまうことを勧める。

On Errorは使わない

VB6時代のエラーハンドリング用構文である「On Error 〜」は現在のVBでも使用できる。
On Error ステートメント (Visual Basic) | Microsoft Docs

エラー処理ルーチンを有効にして、プロシージャ内でルーチンの場所を指定します。また、エラー処理ルーチンを無効にする場合にも使用できます。

On Error ステートメントを使用しないと、発生するすべてのランタイム エラーが致命的なエラーになります。つまり、エラー メッセージが表示され、実行が停止します。

Sub Main()
  On Error GoTo ErrorHandler

  ' 何かエラーが発生する処理
  DoRaiseErrorProcedure()

  Exit Sub

ErrorHandler:
  ' エラー処理

End Sub

だが、On Errorはあくまで「プロシージャ単位」でしかエラーのハンドリングが出来ない上、特定のエラーだけハンドリングするといったこともできない。VB6時代のソースをほとんど流用するといった特殊な場合でない限り、構造化例外処理を使用すべきである。

Visual Basic での構造化例外処理 | Microsoft Docs

以下に構造化例外処理を使った例を示す。

Public Sub Main()
  Try
    ' 何かエラーが発生する処理
    DoRaiseErrorProcedure()
  Catch e As Exception
    ' エラー処理
  Finally
    ' 共通後処理
  End Try
End Sub

構造化例外処理では、Finallyを使用して共通の後処理が行えることも大きな特徴である。

ただし、構造化例外処理もプロシージャ単位で行っていてはOn Errorとほとんど変わらない。プロシージャでは原則として例外のCatchは行わず、「集約例外ハンドラ」で行うようにし、後処理が必要なものは、Finallyで行うことを勧める。

以下にコンソールアプリケーションにて集約例外ハンドラを用いた例外処理の例を示す。

Sub Main()
   Public Sub Main()
    AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomain_UnhandledException
    Try
      ' 何かエラーが発生する処理
      DoRaiseErrorProcedure()
    Finally
      ' 共通後処理
    End Try
  End Sub

  ' 集約例外ハンドラ
  Private Sub CurrentDomain_UnhandledException(ByVal sender As Object, ByVal e As UnhandledExceptionEventArgs)
    Console.WriteLine("致命的なエラーが発生しました。")
    Environment.Exit(-1)
  End Sub

なお、.NET言語の例外処理については、以下のエントリが詳しいのでぜひ一読して欲しい(コードはC#だが意味は通じるはず)。*3
.NETの例外処理 Part.1 – とあるコンサルタントのつぶやき
.NETの例外処理 Part.2 – とあるコンサルタントのつぶやき
.NETの例外処理 Part. 3 – とあるコンサルタントのつぶやき
.NET の例外処理 Part. 4 – とあるコンサルタントのつぶやき

Form.Show()はしない

Windowsアプリケーションにて他のWindowを開きたい場合、通常は以下のように記述する。

Dim subForm As New SubForm()
subForm.Show()

しかし、VBは次のような記述も出来てしまう。

SubForm.Show()

もちろん、C#で同じように書いたらコンパイルエラーとなる。

では、なぜVBではコンパイルエラーとならず、しかも動作してしまうのか?それは「Formの既定のインスタンス」と呼ばれる機能が関係している。この機能は、Visual StudioWindows Formアプリケーションを作成する際、「プロジェクトに含まれるFormクラス」を自動的にインスタンス化し、My.Formsオブジェクトのプロパティとして公開するものだ。

My.Forms オブジェクト (Visual Basic) | Microsoft Docs

現在のプロジェクト内で宣言されている各 Windows フォームのインスタンスにアクセスするためのプロパティを提供します。

つまり、上記のコードは、SubFormクラスの静的なShowメソッドではなく、My.FormsオブジェクトのSubFormプロパティを通じて、SubFormクラスのインスタンスを取得して、そのShowメソッドを呼び出していることになる。

この機能はVB6時代と同じ書き方が出来るように導入されたものと推測するが、Formのインスタンスが「どこ」に属するのかを意識しないため、予期せぬバグを生みやすい。よって、Formは明示的にインスタンス化してShowメソッドを呼ぶようにしたほうが良い。

なお、「Formの既定のインスタンス」を抑止する方法もあるようである。詳しくは以下のエントリをコメントまで含めて参照していただきたい。
VB2005 で「Form の既定のインスタンス」と My の使用を防ぐには?

ByVal,ByRefを明示する

VBはプロシージャの引数に対して、ByVal(値渡し)、ByRef(参照渡し)を指定することができる。
引数の値渡しと参照渡し (Visual Basic) | Microsoft Docs

例を挙げると次のようになる。

Private Sub Hoge(ByVal a As Integer, ByRef b As Integer)
  a = 2
  b = 2
End Sub

Sub Main()
  Dim a As Integer = 1
  Dim b As Integer = 1

  Hoge(a, b)

  Console.WriteLine("{0},{1}", a, b) ' 結果:1,2 
End Sub

ByValを指定した引数に値を設定しても、呼び出し元の変数の値は変わらないが、ByRefを指定した場合、呼び出し元の変数の値が書き換わる。

この、ByVal、ByRefを省略するとどうなるかというと、既定でByValとして扱われる。*4
しかし、一見してどちらの動きとなるかわかりづらいため、ByVal、ByRefは省略せずに書くべきである。

なお、ByVal、ByRefについて、より詳しく知りたい場合は、以下の記事が参考になる。
連載:プロフェッショナルVB.NETプログラミング 第11回 プロシージャとプロシージャ引数(1/3) - @IT

文字列連結は&で行う

VBで文字列を連結するには、「+演算子」か「&演算子」を使う。
Visual Basic の連結演算子 | Microsoft Docs

連結演算子は、複数の文字列を結合して 1 つの文字列にします。 連結演算子には、+ と & の 2 つがあります。

Dim a As String = "1" + "2"
Dim b As String = "1" & "2"

ただ、「+演算子」は数値演算でも使用するため紛らわしい。特に「Option Strict Off」と組み合わさると凶悪さを増すことは前述のとおり。

したがって、文字列を連結する場合、常に「&演算子」を使ってもらいたい。

なお、相当数の文字列をループで連結するような場合*5演算子による連結ではなく、StringBuilderクラスを使用すべきであるので、併せて覚えておいてもらいたい。

StringBuilder Class (System.Text) | Microsoft Docs

連結する String オブジェクトの数が決まっている場合は、String クラスを使用した方が効率的です。 この場合、個々の連結演算は、コンパイラによって 1 つの演算に結合されます。 これに対し、ランダムな数の文字列をユーザーから入力として受け取り、ループ処理で連結する場合など、連結する文字列の数が不定である場合は、StringBuilder オブジェクトが適しています。

プロシージャ呼び出しの()は略さない

VBでプロシージャ(メソッド)を呼び出す際、引数がない場合()を省略できるが、プロパティと紛らわしいため()を省略してはいけない。

Private Function Hoge() As String
  ' 〜
End Function

Public Sub Main()
  Dim hoge As String = Hoge    ' プロパティ?メソッド?
  Dim fuga As String = Hoge()  ' メソッド呼び出しだとすぐわかる
End Sub

なお、Visual Studioでコードを書く場合は、自動的に()が補完される。

Callを使用しない

VBでプロシージャを呼び出す方法として、Callがある。

Call ステートメント (Visual Basic) | Microsoft Docs

Function 、Sub、またはダイナミック リンク ライブラリ (DLL) プロシージャに制御を渡すフロー制御ステートメントです。

Private Function Hoge(ByVal a As String, ByVal b As String) As String
  Return a & b
End Function

Private Sub Fuga(ByVal a As String, ByVal b As String)
  Console.WriteLine(a & b)
End Sub

Sub Main()
  Call Hoge("a", "b")
  Call Fuga("a", "b")

  Hoge("a", "b")
  Fuga("a", "b")
End Sub

だが、Callは付けても付けなくても、結果は同じである。わざわざつける必要はない。*6

長大なWithは避ける

VBの特徴的な記法として、Withがある。

With...End With ステートメント (Visual Basic) | Microsoft Docs

オブジェクトや構造体への参照を繰り返し指定する、一連のステートメントを実行します。

Dim product As New Product()
With product
  .Code = "001"
  .Name = "商品"
End With

Withを使うことで、特定のオブジェクトのメンバへのアクセスをまとめて記述することができる。

だが、入れ子にしたり、Withの中で関係のない処理をしたり、他のステートメントと合わさると、急に凶暴性を発揮する。

Dim org As New Organization()
With org
  .Code = "001"
  .Name = "○○株式会社"

  For Each dept As Department In .Departments
    With dept
      .Code = "001001"  ' org と deptのどっちのメンバ?
      .Name = "ソフトウェア開発事業部"

    End With

  Next

  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 〜かなりの長さの関係ない処理
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 
  ' 

  .Level = Level.Top  ' どのインスタンスのメンバ?
End With

コード例に示したように、「.〜」で指定したメンバが、どのインスタンスのものか非常にわかりにくくなってしまう。*7
Withを使う場合、1画面に収まる程度の長さにし、極力入れ子にしないよう心がけていただきたい。

無意味なNothingの代入

VB6以前では、以下のように変数にNothingを代入することで、オブジェクトを破棄していた。

Set hoge = Nothing

その延長なのか、メソッドの終わりで変数にNothingを代入しているソースをよく見かける。

Sub Main()
  Dim product As New Product()

  ' 〜 処理
  product = Nothing
End Sub

このような記述はVB.NET以降では「意味がない」。なぜならNothingを代入してもオブジェクトは破棄されないからである。*8

また、参照型でなく値型にNothingを代入した場合、暗黙的にその型の初期値が設定される。

Nothing (Visual Basic) | Microsoft Docs

変数に Nothing を代入すると、変数の宣言された型に対する既定値が変数に設定されます。 型に変数のメンバーが含まれている場合は、すべてに既定値が設定されます。

Dim i As Integer = Nothing ' 0
Dim b As Boolean = Nothing ' False

そんな「暗黙の既定値」に頼ったコードは可読性も悪く、思わぬバグも生みかねない。したがって、まったく意味がない上に余計な混乱を招く可能性があるNothingの代入は行わないようにしてもらいたい。


なお、IDisposableインターフェースを実装した型はDisposeメソッドを呼び出すことで、明示的に「アンマネージ リソース」を解放する。

IDisposable.Dispose Method (System) | Microsoft Docs

Usingステートメントと併用して、原則的にDisposeを必ず呼ぶようにしてもらいたい。

Using ステートメント (Visual Basic) | Microsoft Docs

Using conn As new SqlConnection()

  ' 〜 connを使った処理

End Using ' ブロックを抜けるときに必ずconn.Disposeメソッドが呼ばれる

まとめ

VB6以前とVB.NET以降は「別の言語」。その言語にそった正しい記述を心がけよう。

*1:場合によっては数百から千行を超える。

*2:"12"が数値として扱われるため、正解は46になる。

*3:上記コードも紹介したエントリを参考にさせていただいた。

*4:VB6時代はByRefとして扱われていた。

*5:CSVフォーマットのテキストを生成する、などが考えられる。

*6:私の考え。人によってはCall付けた方が見やすいって人もいると思う。

*7:親Withのメンバには子Withの中では「.〜」ではアクセスできないけれど。

*8:オブジェクトの破棄はGC(ガベージ・コレクタ)によって自動的に行われる。ごくまれにGCに回収されやすくするためにNothingを代入することもあるが、そんなのレアケース。