本記事はVisual Basic アドベントカレンダー 2024 に参加しています。
Strict Off について
VB.NETには strict off
というオプションがあります。
offにするとどうなるかというと、コンパイラが気を利かせて、型の変換を暗黙でやってくれるようになります。一例として、以下のようなコードがエラーにならなくなります。
Function Greet(name As String) As String
Return $"Hello, {name}!"
End Function
Greet(123) 'Stringなのに、Integerを渡せてしまう!
Function Pow(x As Integer) As Integer
Return x * x
End Function
Pow("2") 'Integerなのに、Stringを渡せてしまう!
Pow(2.5D) 'Decimal型ももちろん(?)渡せてしまう
Function DateToString(d as Date) As String
Return d.ToString("yyyy/M/d")
End Function
DateToString("2024/12/7") 'Date型なのに、Stringを渡せてしまう!
Dim dt As Date = "2024/12/7" 'なんならこれもできる
Dim dt2 As Date = "hogefuga" 'これもエラーにはならない(実行時例外)
みなさんの思ってたより「暗黙」の範囲が広かったのではないかなと思います。それぐらいStrict OffのVB.NETは型に関してルーズな言語です。
型がルーズというのはとりあえず動くものを作りたい初学者にとってはよいのですが、開発をする立場では、実行時エラーを潰していくのは辛く、静的検査で解決できるなら解決したい問題です。また、暗黙の型変換はパフォーマンスも悪化します(VB.NETのプロジェクトであまり気にすることではないかもしれませんが)
なので、現在は全く推奨されないオプションです。新規プロジェクトを立ち上げる場合には必ずOnにしましょう。というか、今から.NETアプリケーションを新規で作るなら、C#を選択しましょう。
とはいえ、VB.NETの資産と戦わなくてはならないエンジニアもいるはずです。
そのような場合に、Strict Offのコードとどう戦うかを書いていきます。
ハマり例
Strict Offの弊害として、筆者がハマった例があります。このようなコードでした。
Public Function StringToDate(str As String) As Date
Dim result as Date
Dim Success = DateTime.TryParse(str, result)
If Success Then
Return result
End If
Return DateTime.ExactParse(str, "yyyyMMdd", Nothing)
End Function
文字列を日付に変換するメソッドです。実際はもっと複雑なパース処理が手で書いてありました。
で、こいつはStrict OffだとDate型も受け付けてしまいます。なので、StringかDateか怪しいものは、とりあえずこいつを通してDateにする、という書き方になっていました。
一見して(ムダは多いが)問題はなさそうですが、実は直感と異なる挙動をする場合があります。このようなパターンでした。
Dim dt = row("CreateDate") 'Object型、2024/12/7 12:34:56.123 だとすると
'型がわからないので変換
Dim dt2 = StringToDate(dt) 'Date型、2024/12/7 12:34:56.000 になってしまう!
Dim Same = (dt = dt2)
Console.WriteLine(Same.ToString()) ' Falseになる!
StringToDateを通すと日付が変わってしまっています。これは、Date型をStringに暗黙の型変換(ToStringのコール?)する際に、ミリ秒以下の情報が失われるためです。これが原因で日付が一致しない現象に悩まされた経験があります……。
このように、暗黙の型変換はバグの元です。
警告にする
じつはOption Strict Offでも、型変換のエラーを警告として表示できます。これでビルドの通る状態を保ちつつ、望ましくない書き方に気づきやすくなり、他の開発者に修正を促すことが……
……できるんですが、これはあまりオススメしません。
警告なので、コンパイルは通ってしまいます。大量の警告が発生し、しかもすぐに解消できないとなると、大量の警告が出ている状態が常態化してしまいます。こうすると、警告の価値が低下し、真に危険な警告を無視する状態を生みやすいからです。
また、警告を消すために適当に修正していくと、あらたな不具合を生み出す原因にもなりかねません。
警告の量が少なく、発生したら都度修正できるような場合にのみおすすめします。
とりあえずStrict Onにしてみて、修正できるところは修正する
たとえば以下の点はすぐに修正できます。
- Object型で、型が明らかなものはつけ直す
- Char型は
"0"c
と書く(String.Split, String.PadLeftなど) - Long型はLを、Decimal型はDをつける
- String型が要求される場合は.ToStringする(暗黙の型変換も基本的にToStringを使用するので、破壊的変更のおそれが少ない)
全部を一気に修正するのは難しいと思うので、できるところだけ修正したらStrictをOffに戻して、それで良いことにして、ちょっとずつ修正していきます。全部いっきにやろうとしないのが大事です。
ファイル単位でStrict Onにする
Strictはファイル単位で制御できます。ファイルの先頭に以下のように書くだけで、プロジェクトの設定よりも優先して設定できます。
Strict On
ユーティリティ関数などだけでもファイルに切り出して、そこだけでもStrict Onにしておけば、あらぬ不具合を防ぐことができます。
また、逆に、Strict=Onにできない(遅延バインディングの避けられない、DataTable
やInterop.Excel
の操作など)処理をファイル単位で隔離し、Strict Offに設定してから、プロジェクト全体をStrict Onにするアプローチを取ることもできます。
オーバーロード+Obsoleteで型安全に
メソッドを型安全にしても、使う側のStrictがOffだと意味がありません。これをオーバーロードで改善する手法があります。
VB.NETは、型が完全に一致するオーバーロードがあればそれを使用し、ないときに代替のオーバーロードを探しに行きます。さっきのPow関数を例に取れば……
Function Pow(x As Integer) As Integer
Return x * x
End Function
Function Pow(x As Double) As Double
Return x * x
End Function
Function Pow(x As Decimal) As Decimal
Return x * x
End Function
<Obsolete("xを適切な数値型に変換して使用してください。")>
Function Pow(x As String) As Integer
Return Pow(Conversions.ToInteger(x))
End Function
PowにIntegerだけでなく、Double, Decimal, Stringバリエーションも用意しました。これでビルドし直せば、暗黙の型変換が減ります。
また、String型のオーバーロードにはObsoleteという属性がつきました。これをつけると、使用する側に警告が出て、開発時に望ましくない使用方法がわかるようになります。使用箇所すべてを一気に修正できないときに、特に有用です。
ただし、動作は変えないほうがいい
先ほどのStringToDateをオーバーロードで解決すると、こんな感じです。
Public Function StringToDate(str as String) As Date
Dim result as Date
Dim Success = DateTime.TryParse(str, result)
If Success Then
Return result
End If
Return DateTime.ExactParse(str, "yyyyMMdd", Nothing)
End Function
<Obsolete("String型であることを確認して、Stringに変換して使用してください")>
Public Function StringToDate(obj as Object) As Date
Return StringToDate(obj.ToString)
End Function
<Obsolete("Date型の変数に変換を行う必要はありません")>
Public Function StringToDate(dt as Date) As Date
Return dt '?
End Function
Date型のオーバーロードでdt変数をそのまま返していますが、これでいいかは検討の必要があります。というのは、StringToDateにDate型を通すとミリ秒以下の情報が失われますが、それを前提として動いている処理がないかどうか確かめる必要があるからです。
小さいですが破壊的変更には変わりありません。特に共有のライブラリなどでは、この手の修正には慎重になる必要があります。次のほうが無難です。
<Obsolete("Date型の変数に変換を行う必要はありません")>
Public Function StringToDate(dt as Date) As Date
Return StringToDate(dt.ToString())
End Function
VBコンパイラが中でどのように型変換をしているか知る必要があるため、動作を変えずにオーバーロードを足すのは案外難しいです。
DataTableを駆逐する
厳格な型付けをしたいときに一番厄介なのはobject型の存在で、object型を扱わなければならない原因の第一位はDataTableです(筆者の主観です)。DataTableを駆逐すればそれだけ型を気にする機会も増えるので、できれば非常に有効な手法です・
一例としてDapperを使う方法を紹介します。たとえば次のようにゴリゴリSQLを書いてDataTableをいじる処理があったとして……
Dim UserMap = New Dictionary(Of Integer, String)
Dim sql = "Select user_name, id from user_tbl"
Dim conn = New SqlConnection(connectionString)
Dim cmd = conn.CreateCommand()
cmd.CommandText = sql
Dim dt = New DataTable()
dt.Load(cmd.ExecuteReader())
For Each row As DataRow in dt.Rows
UserMap.Add(row("id"), row("user_name")) '暗黙の型変換が発生
'UserMap.Add(CInt(row("id")), row("user_name").ToString()) 'Strict On ならこう書かないとだめ
Next
Dapperを使えば、次のようにList(Of UserModel)で処理をすることができます。これにより型検査の恩恵を受けられるほか、typoのおそれが減るメリットもあります。いいことだらけですね。
Class UserModel
Public Property id As Integer = 0
Public Property user_name as String = ""
End Class
Dim UserMap = New Dictionary(Of Integer, String)
Dim sql = "Select user_name, id from user_tbl"
Dim conn = New SqlConnection(connectionString)
Dim UserList = conn.Query(Of UserModel)(sql).ToList()
For Each user In UserList
UserMap.Add(user.id, user.user_name) '型推論が効く!
Next
ただし、DataGridViewで使う場合は動作に問題がないか検討が必要です。データベースと密に連携しているようなアプリケーションの場合は、DataTableを使わざるを得ないかもしれません。
おわりに
長々と書いてきましたが、実際は「触らぬ神に祟りなし」で、動いているものをむやみに変更すべきではない、という判断を下すことも多いかもしれません。ですが修正の必要なとき、新しいものを書くとき、まずはファイル単位からStrict Onを導入してみてはいかがでしょうか。
この記事が誰かのお役に立てれば幸いです。