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()
では、なぜVBではコンパイルエラーとならず、しかも動作してしまうのか?それは「Formの既定のインスタンス」と呼ばれる機能が関係している。この機能は、Visual StudioでWindows 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
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を代入することもあるが、そんなのレアケース。