VB.NET入門:Option Strictと型、Nothingの落とし穴を.NET 10で検証
VB.NETの基礎を.NET 10で実行。Option Strict OnのBC30512、Offでの実行時InvalidCastException、オーバーフロー例外、/と\、Nothing=0、遅延バインディングの実測165倍。
シリーズ:VB.NET 講座 1/6(初級)。 VB.NET をこれから触る人、Excel VBA や VB6 の経験があって業務システムの VB.NET 保守を任された人向けです。読み終えると、正しいコンパイラオプションでプロジェクトを作り、型変換・割り算・オーバーフロー・Nothing で VB が何をするかを予測できるようになります。
VBA から来た人ほど VB.NET の「優しさ」に助けられ、そして同じ優しさに足をすくわれます。文字列を黙って数値に変え、切り捨てのつもりが四捨五入(正確には偶数丸め)になり、空文字列と Nothing を等しいと判定します。この記事では、そのすべてを .NET 10 で実際に動かし、日本語版 SDK が出す BC エラーの原文と実行時例外をそのまま載せ、最後に「緩いコード」の速度コストを実測します。
環境: .NET SDK 10.0.403、ランタイム 10.0.14、Visual Basic 言語バージョン 17.13(SDK の既定)、Apple Silicon Mac (M1 Pro)。時間計測は Release ビルドです。
.NET 10 で VB プロジェクトを作る
VB.NET は今も .NET SDK に同梱されているので、追加インストールは要りません。
dotnet new console -lang VB -o hello
cd hello
dotnet run
テンプレートが作るのは Program.vb と最小限の hello.vbproj だけで、4つの Option 設定はファイルに書かれていません。MSBuild に実際の値を聞いてみました。
dotnet msbuild -getProperty:OptionStrict -getProperty:OptionExplicit -getProperty:OptionInfer -getProperty:OptionCompare
| Option | テンプレートの値 | 何を制御するか |
|---|---|---|
Option Explicit |
On | 変数は Dim で宣言が必要 |
Option Strict |
Off | 暗黙の縮小変換と遅延バインディング |
Option Infer |
On | Dim x = 10 を Integer と推論 |
Option Compare |
Binary | "A" = "a" は False |
問題は Option Strict が Off であること。プロジェクト全体で On にするには .vbproj に1行足します。
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<OptionStrict>On</OptionStrict>
</PropertyGroup>
ファイル先頭に Option Strict On と書けば、そのファイルだけ設定を上書きできます。言語バージョンはコンパイラ(vbc -langversion:?)に聞くと 16.9、17.13 (default)、latest で終わる一覧が出ました。VB は保守は続いているものの大きな新機能は入らないので、VB 16 時代の知識がそのまま通用します。
Option Strict Off:コンパイルは通り、本番で落ちるコード
テンプレートの既定のままビルドした小さなプログラムです。
Imports System
Module Program
Sub Main()
Dim o As Object = "42"
Dim n As Integer = o ' Object -> Integer をキャストなしで
Console.WriteLine($"n = {n}")
Dim bad As Object = "12a"
Try
Dim m As Integer = bad
Catch ex As Exception
Console.WriteLine($"{ex.GetType().FullName}: {ex.Message}")
End Try
Console.WriteLine("1" + 2)
Console.WriteLine("1" & 2)
Console.WriteLine("1" + "2")
End Sub
End Module
警告ゼロでビルドが通り、こう表示されます(実行時メッセージは今回の環境では英語で出ました)。
n = 42
System.InvalidCastException: Conversion from string "12a" to type 'Integer' is not valid.
3
12
12
5行で罠が2つあります。1つ目、Object から Integer への変換は実行時にしか検査されないので、バグが見つかるのはユーザーが 12a と入力したときです。2つ目、+ はオペランドの型で意味が変わります。"1" + 2 は数値の 3、"1" + "2" は文字列 "12"、& は常に連結。VBA でも & を使えと言われてきた理由がここにあります。
Option Strict On:日本語版 SDK が出す BC エラー
先頭に Option Strict On を置き、初心者がよく書く行を足します。
Option Strict On
Imports System
Module Program
Sub Main()
Dim o As Object = "42"
Dim n As Integer = o
Dim big As Long = 5_000_000_000
Dim i As Integer = big
Dim k As Integer = 7 / 2
Dim s As String = "1" + 2
Dim c As Char = "A"
Dim r = o.Length
End Sub
End Module
日本語 UI の SDK では、dotnet build がこう拒否します。
Program.vb(7,28): error BC30512: Option Strict On では 'Object' から 'Integer' への暗黙的な変換は許可されていません。
Program.vb(9,28): error BC30512: Option Strict On では 'Long' から 'Integer' への暗黙的な変換は許可されていません。
Program.vb(10,28): error BC30512: Option Strict On では 'Double' から 'Integer' への暗黙的な変換は許可されていません。
Program.vb(11,27): error BC30512: Option Strict On では 'String' から 'Double' への暗黙的な変換は許可されていません。
Program.vb(11,27): error BC30512: Option Strict On では 'Double' から 'String' への暗黙的な変換は許可されていません。
Program.vb(12,25): error BC30512: Option Strict On では 'String' から 'Char' への暗黙的な変換は許可されていません。
Program.vb(13,17): error BC30574: Option Strict On では、遅延バインディングを使用できません。
11行目のメッセージに注目してください。"1" + 2 は Double として計算されている、とコンパイラが教えてくれています。10行目は 7 / 2 も Double だということ。13行目は遅延バインディングで、Object の o.Length は実行時に名前で探されます。直し方はどれも明示的に書くことです:CInt(o) か Integer.TryParse、CInt(big)、7 \ 2、"1" & 2、"A"c、DirectCast(o, String).Length。直したファイルはエラーなしでビルドでき、次を表示しました。
CInt(big): OverflowException
42 3 12 A 2 TryParse("12a")=False v=0
1行目に注意。大きすぎる Long に明示的な CInt を使っても切り捨てられず、例外になります。
Option Explicit Off:打ち間違いが新しい変数になる
Option Explicit は既定で On ですが、古い業務コードでは Off になっていることがあります。
Option Explicit Off
Imports System
Module Program
Sub Main()
count = 0
For Each w In {"a", "b", "c"}
count = count + 1
Next
Console.WriteLine($"items: [{cuont}]") ' 打ち間違い
End Sub
End Module
warning BC42104: 変数 'cuont' は、値が割り当てられる前に使用されています。Null 参照の例外が実行時に発生する可能性があります。
items: []
警告だけで、空の値が表示されます。Option Explicit On なら error BC30451: 'cuont' は宣言されていません。 で止まります。
数値:割り算、丸め、オーバーフロー
VB の割り算演算子は2つあり、C# や Java から来た人が必ず驚きます。
Console.WriteLine($"7 / 2 = {7 / 2}, 7 \ 2 = {7 \ 2}, -7 \ 2 = {-7 \ 2}, -7 Mod 2 = {-7 Mod 2}")
' Option Strict Off の場合
Dim k As Integer = 7 / 2
Dim k2 As Integer = 5 / 2
Console.WriteLine($"{k} {k2}")
Console.WriteLine($"CInt(2.5)={CInt(2.5)} CInt(3.5)={CInt(3.5)} Fix(-2.5)={Fix(-2.5)} Int(-2.5)={Int(-2.5)}")
7 / 2 = 3.5, 7 \ 2 = 3, -7 \ 2 = -3, -7 Mod 2 = -1
4 2
CInt(2.5)=2 CInt(3.5)=4 Fix(-2.5)=-2 Int(-2.5)=-3
/ の結果は常に Double、\ は整数除算でゼロ方向に切り捨てます。Strict Off で 7 / 2 を Integer に入れると、切り捨てではなく偶数丸め(銀行型丸め)になり、7 / 2 は 4、5 / 2 は 2 です。「単価 ÷ 個数」のような計算を C# から移植して / のまま残すと、ここで1円ずれることがあります。
オーバーフローは既定で検査される
同じ足し算を VB と C# で動かしました。
Dim a As Integer = Integer.MaxValue
a += 1 ' VB
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
C# 版(int a = int.MaxValue; a += 1;)は C#: a = -2147483648 と表示しました。VB は明示しない限りオーバーフロー検査付きでコンパイルします。.vbproj に <RemoveIntegerChecks>true</RemoveIntegerChecks> を足すと、VB でも a = -2147483648 になりました。検査は残すのがおすすめです。分かりやすい例外で落ちるほうが、マイナスの合計金額よりずっと直しやすい。Byte の 255 + 1 も同じ例外でした。
金額には Decimal、計測値には Double
Double : 0.30000000000000004 (= 0.3? False)
Decimal: 0.3 (= 0.3? True)
0.1 x 1000 -> Double 99.9999999999986, Decimal 100.0
0.1 + 0.2 と、0.1 を1000回足した結果です。請求書や消費税の計算のように人が検算する数値は Decimal(接尾辞 D)、センサー値や科学計算は Double。リテラルの型も GetType().Name で確かめました:1 は Int32、1L は Int64、1.5 は Double、1.5D は Decimal、1.5F は Single、"x"c は Char。
文字列、Char、そして Nothing
String は参照型、Char は UTF-16 の1コード単位で値型です。Char リテラルには c が必要で、"A" だけだと1文字の文字列になり、Strict On では Char に代入できません(上の BC30512)。
一番混乱するのが Nothing です。VB の Nothing は「null」ではなく「その型の既定値」を意味します。
Dim n As Integer = Nothing
Dim flag As Boolean = Nothing
Dim dt As Date = Nothing
Dim s As String = Nothing
Dim ni As Integer? = Nothing
Dim empty As String = ""
Integer=0 Boolean=False Date=0001-01-01 String Is Nothing=True Integer?.HasValue=False
"" = Nothing : True
"" Is Nothing: False
Len(Nothing string) = 0
s.Length -> NullReferenceException
Dim n As Integer = Nothing はただの 0 です。DB の NULL のように「数値がない」状態を表すなら Integer? を使います。文字列では = が Nothing と "" を等しいとみなし、Is は本当に null 参照かを調べます。VB の Len(s) は Nothing でも 0 を返しますが、s.Length は例外です。
Is と = の違い
= は値を比べ、Is は参照(同じオブジェクトか)を比べます。
x = y : True x Is y : False ' 別々に作った "abc"
o1 Is o2 : False o1 IsNot o2 : True
Strict On では、取り違えるとコンパイラが止めます。
error BC30020: 'Is' 演算子は型 'Integer' のオペランドを受け付けません。オペランドは参照型または Null 許容型でなければなりません。
error BC32013: Option Strict On が指定されている場合、演算子 '=' に対して Object 型のオペランドを使うことはできません。オブジェクト ID をテストするには、'Is' 演算子を使用してください。
大文字小文字を区別しない識別子と Option Compare Text
VB の識別子は大文字小文字を区別しません。次のコードはコンパイルが通り、total = 6 と表示します。
Dim Total As Integer = 5
TOTAL += 1
console.writeline($"total = {total}")
文字列の比較は別の話です。既定の Option Compare Binary では "A" = "a" は False。古いファイルの先頭に Option Compare Text があると、こうなります。
"A" = "a" : True
String.Equals: False
InStr: 3 IndexOf: -1
VB の演算子や関数(=、InStr)はファイルの設定に従い、.NET のメソッド(String.Equals、IndexOf)は従いません。同じファイルで混ぜると、「一致するか」に2つの答えが出ます。
よくある間違い:「テストでは動いた」
間違い。 画面のテキストボックスから数量を Object(または直接 Integer)に Strict Off のまま読み込む。5、20、100 で試して問題なし。
実際に起きること。 利用者が 12a と入力した瞬間、ただの代入行で InvalidCastException: Conversion from string "12a" to type 'Integer' is not valid. が出ます。前後の空白は問題なく、CInt(" 12 ") は 12 を返しました。落ちるのは純粋に数字でない文字が入ったときです。
対処。 <OptionStrict>On</OptionStrict> にし、BC30512 が示す「黙って変換していた場所」を一覧として使い、ユーザー入力の箇所を Integer.TryParse(text, value) に置き換えます。
判断表
| 場面 | 使う | 避ける |
|---|---|---|
| 文字列の連結 | & |
+(片方が数値だと加算) |
| 整数の割り算 | \ |
/ を Integer に代入(偶数丸め) |
| 金額・合計 | Decimal(D) |
Double |
| 「値なし」の数値 | Integer? |
Integer = Nothing(0 になる) |
| null チェック | Is Nothing / IsNot Nothing |
文字列の = Nothing("" も一致) |
| 同じオブジェクトか | Is |
Object 同士の =(BC32013) |
| 入力値の解析 | Integer.TryParse |
暗黙変換、生の文字列に CInt |
| 新規プロジェクト | <OptionStrict>On</OptionStrict> |
テンプレート既定(Off) |
実測:遅延バインディングのコスト
Strict Off では Object 型変数のメソッドも呼べますが、その呼び出しは毎回、実行時に名前で解決されます。同じメソッドを100万回呼んで計りました。コードは英語版と共通の1ファイルです(Counter クラスの Add を、型付き変数・Object 変数・Object の算術で呼び比べる)。
Option Strict Off
Imports System
Imports System.Diagnostics
Public Class Counter
Public Total As Long
Public Sub Add(x As Integer)
Total += x
End Sub
End Class
Module Program
Function Early(n As Integer) As Long
Dim c As New Counter()
For i = 1 To n
c.Add(i)
Next
Return c.Total
End Function
Function Late(n As Integer) As Long
Dim c As Object = New Counter()
For i = 1 To n
c.Add(i) ' 実行時に解決される
Next
Return c.Total
End Function
Function Boxed(n As Integer) As Long
Dim total As Object = 0L ' Object での算術
For i = 1 To n
total = total + i
Next
Return total
End Function
Sub Time(name As String, f As Func(Of Integer, Long), n As Integer)
f(10_000) ' ウォームアップ
For run = 1 To 3
Dim sw = Stopwatch.StartNew()
Dim r = f(n)
sw.Stop()
Console.WriteLine($"{name,-7} run {run}: {sw.Elapsed.TotalMilliseconds,8:F1} ms (result {r})")
Next
End Sub
Sub Main()
Const N As Integer = 1_000_000
Time("early", AddressOf Early, N)
Time("late", AddressOf Late, N)
Time("object", AddressOf Boxed, N)
End Sub
End Module
dotnet build -c Release のあと DLL を直接実行、Apple Silicon Mac (M1 Pro):
| 方式 | 1回目 | 2回目 | 3回目 |
|---|---|---|---|
事前バインディング(Dim c As New Counter) |
2.2 ms | 2.1 ms | 2.2 ms |
Object での算術 |
31.4 ms | 31.1 ms | 31.0 ms |
遅延バインディング(Dim c As Object) |
752.5 ms | 359.6 ms | 364.5 ms |
結果はどれも 500000500000 で同じです。遅延バインディングはウォームアップ後でも事前バインディングの約165倍遅く、最初の計測回はさらに倍かかりました(小さなウォームアップでは遅延バインダーのキャッシュが埋まりきらなかったためと考えられます)。別プロセスでもう一度動かすと3回目は 2.2 / 32.4 / 374.2 ms で、傾向は同じでした。Strict On なら遅い2つはどちらもコンパイルエラーになる。それがこの設定の価値です。
正直な限界・バージョンの注記
- 上のコードはすべて macOS 上の .NET SDK 10.0.403(ランタイム 10.0.14、VB 17.13)でビルド・実行しました。Windows の Visual Studio や .NET Framework 4.x では動かしていません。Option Strict などの規則は VB.NET 初版(2002年)からあるので同じはずですが、確認したのは .NET 10 だけです。
- コンパイラのメッセージは SDK の UI 言語で出ます。この記事の日本語メッセージは
DOTNET_CLI_UI_LANGUAGE=jaで取得しました。BC 番号はどの言語でも同じなので、検索するときは番号で探すのが確実です。 - 時間は1台・1種類のループでの値です。ミリ秒ではなく比率を持ち帰ってください。
RemoveIntegerChecksは.vbprojを書き換えて確認しました。dotnet runに-p:で渡した場合、今回の試行では結果が変わらなかったので、プロジェクトファイルに書いてください。
次の講座: VB.NET 講座 2/6:プロシージャ、ByVal/ByRef と制御構文。C# と比べたい人は C# の変数とデータ型 と C# の演算子、Java なら Java 入門:型・文字列・等価性 が同じ範囲を扱っています。
よくある質問
新しいVB.NETプロジェクトでは Option Strict を On にすべきですか?
はい。.NET 10 のコンソールテンプレートでは Off のままです。.vbproj に <OptionStrict>On</OptionStrict> を足すと、入力 "12a" で実行時に InvalidCastException になっていたコードが、ビルド時の BC30512 として見つかりました。
VB.NET は Integer のオーバーフローで例外を出しますか?
既定で出します。.NET 10 で Integer.MaxValue + 1 は System.OverflowException になり、同じ式の C# は黙って -2147483648 に回り込みました。<RemoveIntegerChecks>true</RemoveIntegerChecks> で C# と同じ挙動になります。
Integer に Nothing を代入すると何になりますか?
その型の既定値です。Dim n As Integer = Nothing は 0、Boolean は False、Date は 0001-01-01 になります。本当に「値なし」を持てるのは参照型と Integer? だけです。