文字列数値変換の罠:CInt・CLng・Valの暗黒面と、モダン.NETにおけるTryParseへの絶対的移行
レガシーなVB6(Visual Basic 6.0)の時代から、私たちは数々の「文字列から数値への変換」を行ってきた。
VBAからVB.NETへのマイグレーション現場、あるいは数十年稼働し続ける社内基幹システムの保守において、最も軽視され、そして最も多くのアプリケーションクラッシュを引き起こしてきた元凶は何だと思うか?
それは、`CInt`、`CLng`、そして `Val` という一見無害に見える関数群である。
本稿では、これらレガシーな変換関数の内部挙動における決定的な差異を解剖し、なぜそれらがモダンな業務アプリケーションにおいて「地雷」と化すのかを、オブジェクトのライフサイクルと例外処理の観点から徹底的に解説する。さらに、現代の.NET(.NET Framework / .NET Core / .NET 8)において唯一推奨される `TryParse` パターンへの完全移行について、チーフアーキテクトとしての知見を授けよう。
—
1. レガシー関数たちの正体:CInt, CLng, Val の挙動差異
まずは、それぞれの関数が「何を考え、どう振る舞うのか」を正確に把握する必要がある。なんとなく動いているコードほど、異常系(不正な入力値)に直面した瞬間にシステムを崩壊させる。
① `CInt` と `CLng`:言語ランタイムによる厳格な型変換(だが例外で落ちる)
これらはVB.NETの言語ランタイム(`Microsoft.VisualBasic` 名前空間)に属する関数であり、内部的には `.NET` の `Convert.ToInt32` や `Convert.ToInt64` に近い動作をする。
- 最大の特徴: 空文字(`”” `)を渡すと `0` に変換してくれるという「優しさ」を持つが、数値に変換できない文字列(例: `”123A”` や `”ABC”`)を渡した瞬間、容赦なく `InvalidCastException`(無効なキャスト例外) を発生させてアプリケーションを強制終了させる。
- オーバーフロー: 扱える数値の範囲(`CInt` なら -2,147,483,648 ~ 2,147,483,647)を超えると、これも容赦なく `OverflowException` を吐き捨てる。
② `Val`:VB6の亡霊、予測不可能なパース関数
`Val` 関数は、VBA/VB6時代からの互換性のためにVB.NETに残された遺物である。その挙動は極めて特殊であり、モダンなシステム開発においては百害あって一利なしの存在と言える。
- 最大の特徴: 文字列の左から順に数値とみなせる文字を読み取り、数値化できない文字にぶつかった時点でパースを打ち切り、そこまでの結果を返す。
- `Val(“12345ABC”)` ➔ `12345`
- `Val(“100円”)` ➔ `100`
- `Val(“A100”)` ➔ `0` (先頭が数値ではないため)
- 最大の罠: ロケール(文化圏)を無視し、小数点として「ピリオド(`.`)」しか認識しない。日本語環境のOSでカンマ区切りの文字列などを処理させると、予期せぬ誤算を生む。エラーを吐かずに勝手に中途半端な数値を返すため、サイレントバグ(気づきにくいバグ)の温床となる。
—
2. 比較検証:なぜレガシー関数は業務システムで使ってはならないのか
以下のコードを見てほしい。レガシーな発想で書かれたコードと、堅牢なシステムを支えるコードの決定的な違いがここにある。
.net
‘ 【アンチパターン】レガシーな変換関数を用いた危険な実装
Module LegacyConverterRisk
Public Sub ProcessUserInput(inputStr As String)
Try
‘ CIntは “123A” などのゴミデータで即座に例外(クラッシュ)を引き起こす
‘ Dim val1 As Integer = CInt(inputStr)
‘ Valはゴミデータを勝手に切り詰めて処理を続行してしまう(データの破損)
Dim val2 As Double = Val(inputStr)
Console.WriteLine($”Valの結果: {val2}”)
Catch ex As Exception
‘ 例外キャッチしているから安全? いや、例外処理はコストが高い
Console.WriteLine($”エラー発生: {ex.Message}”)
End Function
End Sub
End Module
例外(Exception)を制御フローに使うな
プログラミング初級者がやりがちな間違いとして、「エラーが出たら `Try-Catch` で捕まえればいいや」という思想がある。
しかし、シニアエンジニアであれば常識として知っている通り、例外の発生とキャッチはCPUおよびメモリに対して極めて高コストな処理である。ユーザーが不正な入力値を意図的、あるいは偶発的に頻繁に入力するUI層において、例外をフロー制御の代わりに使う設計は、ガベージコレクション(GC)の負担を増やし、アプリケーション全体のスループットを確実に低下させる。
—
3. モダンの極意:`TryParse` パターンによるゼロ例外設計
.NET Framework 2.0 以降、そして現在の .NET 8 に至るまで、数値変換のゴールドスタンダードは `TryParse` メソッド である。
`TryParse` は例外をスローしない。変換が成功すれば `True` を返し、失敗すれば `False` を返す。これによって、例外コストを完全に排除した「高速で安全な分岐処理」が可能になる。
実践:堅牢な入力値検証とパースの実装
以下に、現場でそのまま使える堅牢な拡張メソッドおよび実装パターンを示す。
.net
Imports System
Module ModernParser
Sub Main()
‘ テストケース
Dim testValues As String() = {“12345″, ” -99.8 “, “123ABC456”, “”, Nothing, “999999999999”}
For Each val In testValues
Console.WriteLine($”入力値: ‘{If(val, “Nothing”)}'”)
Dim result As Integer
‘ TryParseを使った安全かつ高速な変換
If Integer.TryParse(val, result) Then
Console.WriteLine($” -> 変換成功! 整数値: {result}”)
Else
Console.WriteLine($” -> 変換失敗。デフォルト値またはエラー処理を実行します。”)
End If
Console.WriteLine(New String(“-“c, 40))
Next
End Sub
End Module
`TryParse` を使うべき圧倒的なアドバンテージ
1. ゼロ例外(Zero Exceptions): 失敗時に例外オブジェクトが生成されないため、メモリ消費とCPUサイクルを極限まで抑制できる。
2. 空白やトリムのハンドリング: 前後の空白文字(`” 123 “` など)は自動的にトリムされて正しくパースされる。
3. オーバーフローの安全な検知: 型の最大値・最小値を超える値が入力された場合でも、例外を吐かずに単に `False` を返すため、安全にフォールバック処理へ移行できる。
4. 詳細な制御(NumberStyles と IFormatProvider): カンマ区切り(`”1,234″`)や通貨記号、16進数などを厳密にパースしたい場合、以下のようにオプションを指定できる。
.net
Dim numberStr As String = “1,234”
Dim parsedValue As Integer
‘ カンマ(Thousands)を許可したパース
If Integer.TryParse(numberStr, System.Globalization.NumberStyles.AllowThousands Or System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, parsedValue) Then
‘ 成功時の処理
End If
—
4. チーフアーキテクトからの提言:レガシーコードからの脱却ロードマップ
社内システムやVBAからのコンバージョン案件において、既存のソースコードには大量の `CInt`, `CLng`, `Val` が埋もれていることだろう。「動いているから触るな」という現場の声を跳ね返し、システムをモダン化するためのロードマップを提示する。
1. 静的解析ツールの導入: RoslynアナライザーやFxCopを用いて、プロジェクト内の `Microsoft.VisualBasic` 由来の型変換関数を検出し、警告(あるいはビルドエラー)とする。
2. 共通バリデーションレイヤーの構築: 入力値チェックは散在させず、共通のユーティリティクラス(あるいは前述の拡張メソッド)に集約する。
3. マネージドヒープの意識: 大量の文字列処理を行うバッチ処理やAPI連携レイヤーにおいて、不必要な例外スローを排除することは、メモリの断片化(Fragmentation)を防ぎ、LOH(Large Object Heap)の肥大化を抑止する直結の手段となる。
コードの行数を少しケチって `CInt(val)` と書くか、安全性を担保するために数行の `If Integer.TryParse(…) Then` を書くか。
その差が、夜間バッチの突然の異常終了を防ぎ、運用保守コストを劇的に削減するエンジニアの「技量と誇り」の境界線なのである。
妥協のないコードを書け。それがプロフェッショナル・エンジニアの流儀だ。
