VB.NETでユーザー入力や外部データを扱うと、「ASCII以外の文字(日本語・アクセント付き文字・絵文字など)が混ざっていないか」を判定したい場面があります。よく見かける Encoding.UTF8.GetByteCount と String.Length の比較は本当に安全なのか、例外や落とし穴、より明確な代替実装まで実務目線で整理します。
結論:Encoding.UTF8.GetByteCount(inString) <> inString.Length は「非ASCII文字の有無チェック」として基本的に有効
まず結論から言うと、提示された次の判定は「文字列にASCII以外の文字が含まれるか」を見分ける用途で、考え方として成立します。
Encoding.UTF8.GetByteCount(inString) <> inString.Length
理由はシンプルで、UTF-8の仕様上、ASCII(U+0000〜U+007F)は必ず1バイトで表現され、ASCII以外は2〜4バイトになります。一方、.NETのString.Lengthは「UTF-16のコード単位数(16bit単位)」を返します。ASCIIのみで構成される場合、各文字はUTF-16でも1コード単位=UTF-8でも1バイトになり、結果として「UTF-8のバイト数」と「Length」が一致します。
逆に、ASCII以外の文字が1つでも入ると、UTF-8のバイト数が必ず増えるため、GetByteCount と Length は一致しなくなります。したがって、この比較がTrueなら「少なくとも1文字以上の非ASCIIが含まれる」と判断できます。
この判定が成立する仕組みを、UTF-8とString.Lengthの性質から整理
ASCII・UTF-8・UTF-16の関係を押さえる
混乱しやすいポイントを先に整理します。
- ASCII:0〜127(U+0000〜U+007F)の範囲。英数字や基本記号、制御文字(改行やタブ)も含みます。
- UTF-8:Unicode文字を1〜4バイトで表現する可変長エンコーディング。ASCII範囲は1バイト固定。
- UTF-16:Unicode文字を主に2バイト(16bit)単位で表現。BMP(U+0000〜U+FFFF)の多くは1コード単位、絵文字など一部はサロゲートペアで2コード単位になります。
- .NETの
String.Length:一般に「見た目の文字数」ではなく、UTF-16のコード単位数です。
UTF-8のバイト数とUTF-16のコード単位数の対応
「ASCIIだけなら一致し、それ以外は不一致になる」ことを表で確認します。
| 文字の種類 | 例 | Unicode範囲の目安 | UTF-8のバイト数 | UTF-16コード単位数(.Lengthの増え方) | 比較結果(バイト数 vs Length) |
|---|---|---|---|---|---|
| ASCII | A, 0, -, 空白, 改行 | U+0000〜U+007F | 1 | 1 | 一致しやすい |
| アクセント付きなど | é, ü | U+0080〜U+07FF | 2 | 1 | 不一致になる |
| 日本語など(BMP内) | あ, 漢, 語 | U+0800〜U+FFFF | 3 | 1 | 不一致になる |
| 絵文字など(サロゲート) | 😀 | U+10000以上 | 4 | 2 | 不一致になる |
この関係が崩れない限り、提示式は「ASCIIだけで構成されているか」をかなり確実に見分けられます。
「ANSI」と「ASCII」を混同しないのが重要
質問文で「ANSI」という言葉が出てくるケースがありますが、今回のロジックが判定しているのはANSIではなくASCIIです。
- ASCII:0〜127の狭い範囲(英数字・基本記号)。
- ANSI:Windows界隈で「システム既定のコードページ(いわゆる“ローカル8bit”)」を指して使われがちで、Shift-JISなど環境依存で範囲が変わります。
「日本語WindowsならShift-JISで表現できる文字までOK」などを求めている場合、ASCII判定では要件を満たしません。後半で「ANSI寄りの要件」を満たす方法も紹介します。
例外(エラー)が起きるケースと、安全に使う最小構成
例外が起こりうる代表例は Nothing(null)
Encoding.UTF8.GetByteCount(inString) は、引数が Nothing の場合に例外になり得ます。実運用でユーザー入力や外部連携の文字列は Nothing が混ざることがあるため、例外処理よりも事前チェックで安全にするのがおすすめです。
おすすめ:例外に頼らないVB.NET実装
Imports System.Text
Public Module TextUtil
Public Function ContainsNonAscii_ByUtf8ByteCount(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return False
End If
' ASCIIのみなら「UTF-8バイト数 = UTF-16コード単位数」になりやすい
Return Encoding.UTF8.GetByteCount(s) <> s.Length
End Function
End Module
Nothing や空文字をどう扱うかは要件次第ですが、「入力がない=非ASCIIなし」とするなら上記が扱いやすい形です。「未入力はエラーにしたい」場合は戻り値ではなく別のバリデーション設計にするのが安全です。
「通常の文字列」で想定外の例外は基本的に少ない
UTF-8変換は通常の文字列に対して堅牢で、一般的にはNothing以外で例外が頻発することは多くありません。ただし、設計としては「例外が出ないはず」前提ではなく、呼び出し側で入力のnull/空の扱いを決めておくと運用が安定します。
この方法のメリットとデメリットを実務目線で整理
| 観点 | GetByteCount vs Length | コメント |
|---|---|---|
| 正しさ(非ASCII検出) | 高い | UTF-8でASCIIは1バイト固定という性質を利用。非ASCIIが1つでも入ると差が出やすい。 |
| 分かりやすさ | 中 | 「なぜこれで分かるのか」を知らないと意図が読み取りにくい。保守時に説明が必要。 |
| 速度 | 中 | 内部的にUTF-8のバイト数計算を行うため、単純な文字比較より重いことがある。 |
| 早期終了 | できない | 原則として全体を走査し、バイト数を数える。先頭で非ASCIIが見つかっても止まれない。 |
| 割り当て(メモリ確保) | 少ない | GetByteCount自体は「バイト配列を作らずに数える」ため、必要以上の確保は起きにくい。 |
「正しさは十分だが、意図の伝わりやすさと性能面で、別案が勝つことがある」という位置づけです。
より明確で速いことが多い代替案:文字コード値で直接チェックする
最も読みやすい:Charを直接比較する
ASCIIかどうかを判定するだけなら、UTF-8に変換して数を比べるよりも、各文字が0x7F以下かを見た方が意図が明確です。さらに、非ASCIIを見つけた時点で早期終了できます。
Public Function ContainsNonAscii_ByCharScan(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return False
End If
For Each ch As Char In s
If ch > ChrW(&H7F) Then
Return True
End If
Next
Return False
End Function
この書き方は「ASCIIの範囲(0〜127)を超えたら非ASCII」という意図がそのままコードになっています。
LINQで短く書く(短文入力向け)
Imports System.Linq
Dim containsNonAscii As Boolean =
Not String.IsNullOrEmpty(inString) AndAlso
inString.Any(Function(ch) ch > ChrW(&H7F))
短く書けますが、ホットパス(大量処理や高頻度実行)ではFor Eachの方が読みやすく速くなりやすいです。
AscWを使う場合の注意点
VBではAscWでコードを取り出して判定する例も見かけますが、互換性の都合でU+8000以上の文字が負の値として返ることがある点に注意が必要です。例えば、条件を単純にAscW(ch) > 127としてしまうと、環境や文字によっては判定が崩れる可能性があります。
AscWを使うなら、負値も非ASCIIとして扱うなど、意図がブレないように補正します。
Public Function ContainsNonAscii_ByAscW(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return False
End If
For Each ch As Char In s
Dim code As Integer = AscW(ch)
' ASCII(0..127)は必ず非負なので、負値は非ASCII扱いにする
If code < 0 OrElse code > 127 Then
Return True
End If
Next
Return False
End Function
とはいえ、Char比較(ch > ChrW(&H7F))の方がシンプルで事故が少ないため、基本はそちらを推奨します。
「ASCIIのみ」と「表示可能ASCIIのみ」は別物
ASCIIには改行(LF/CR)やタブ、NULLなどの制御文字も含まれます。「ASCIIのみ」を許可したつもりでも、入力に改行が入っていてもOKになってしまうことがあります。
要件として多いのは次の2パターンです。
| 要件 | 許可する範囲 | 例 | おすすめ判定 |
|---|---|---|---|
| 本当にASCIIのみ | 0x00〜0x7F | 改行・タブもOK | ch <= 0x7F |
| 表示可能ASCIIのみ | 0x20〜0x7E | 英数字・記号のみ、改行NG | 0x20 <= ch <= 0x7E |
表示可能ASCIIだけを許可する実装例
Public Function IsPrintableAsciiOnly(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return True
End If
For Each ch As Char In s
If ch < ChrW(&H20) OrElse ch > ChrW(&H7E) Then
Return False
End If
Next
Return True
End Function
「ログインIDは英数字と一部記号だけ」など、より制限を強めたい場合は、この関数を土台にして「許可する文字集合」を追加していくと要件が明確になります。
正規表現でチェックしたい場合のポイント
正規表現は「仕様としての表現」がしやすく、レビューやドキュメント化に向きます。一方で、パフォーマンスやデバッグ性はループ判定に劣ることがあるため、用途で使い分けます。
ASCIIのみを許可する正規表現
Imports System.Text.RegularExpressions
Public Function IsAsciiOnly_ByRegex(ByVal s As String) As Boolean
If s Is Nothing Then
Return False
End If
Return Regex.IsMatch(s, "^[\x00-\x7F]*$")
End Function
表示可能ASCIIのみを許可する正規表現
Public Function IsPrintableAsciiOnly_ByRegex(ByVal s As String) As Boolean
If s Is Nothing Then
Return False
End If
Return Regex.IsMatch(s, "^[\x20-\x7E]*$")
End Function
入力が短く、可読性を重視したい場合に向きます。大量データ処理や高頻度判定では、ループ判定を優先すると安定します。
「ANSI(特定コードページで表現可能か)」を判定したい場合の考え方
もし要件が「ASCIIかどうか」ではなく、「Shift-JIS(など特定コードページ)で表現できるか」を確認したいなら、判定方法は別になります。UTF-8はUnicodeをほぼ全て表現できるため、UTF-8のバイト数比較ではANSI可否は分かりません。
この場合の基本戦略は次のどちらかです。
- 特定エンコーディングでエンコードできるかを試し、表現できない文字があれば弾く
- 要件をさらに具体化して、許可したい文字集合(例:JIS第一水準まで等)を別途定義して検証する
前者を採用する場合、デフォルトのフォールバック(置換)だと「表現できない文字が ? に置き換えられて通ってしまう」ため、例外を投げるフォールバックを使うのがコツです。
Imports System.Text
Public Function CanEncodeInShiftJis(ByVal s As String) As Boolean
If s Is Nothing Then
Return False
End If
Dim sjis As Encoding =
Encoding.GetEncoding(
932,
EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback
)
Try
sjis.GetByteCount(s)
Return True
Catch ex As EncoderFallbackException
Return False
End Try
End Function
例外を使うため、頻繁に失敗する入力(多言語や絵文字が多い入力)に対してはコストが増えます。大量処理では「先にASCII判定」や「許可文字集合のループ判定」など、例外に頼らない設計も検討するとよいです。
実務での使い分け早見表
| やりたいこと | おすすめ | 理由 |
|---|---|---|
| 非ASCIIが含まれるか知りたい | For Eachでch > 0x7F | 意図が明確で早期終了できる |
| 提示式をそのまま使いたい | GetByteCountとLength比較 | ロジックとしては成立。null対策だけ必須 |
| 表示可能ASCIIのみ許可したい | 0x20〜0x7Eでループ判定 | 制御文字を除外できる |
| 仕様を正規表現で表したい | ^[\x00-\x7F]*$など | 要件が文章化され、レビューしやすい |
| Shift-JIS等で表現できるか確認したい | 特定Encoding+例外フォールバック | 置換で誤判定しない |
テストケース集:どんな入力がどう判定されるか
実際に判定を組み込む前に、代表的な入力を一度まとめて確認しておくと運用トラブルが減ります。
| 入力例 | 見た目 | 非ASCII判定(期待) | 注意点 |
|---|---|---|---|
ABC-123 | 英数字 | 含まれない | 典型的なASCII |
Cafe | 英字 | 含まれない | アクセント無しならASCII |
Café | アクセント付き | 含まれる | éは非ASCII |
こんにちは | 日本語 | 含まれる | 当然非ASCII |
😀 | 絵文字 | 含まれる | UTF-16ではサロゲートペアでLengthが2になりやすい |
e + 結合文字 | 見た目はéに近い | 含まれる | 結合文字(アクセント)は非ASCIIなので弾ける |
ABC | 全角英字 | 含まれる | 見た目が似ていても別文字。ASCII制限で防げる |
ABC<改行>123 | 改行を含む | 含まれない(ASCII判定なら) | 「表示可能ASCIIのみ」の要件なら別チェックが必要 |
デバッグ用:判定結果を一覧で確認するVBコード例
Imports System.Text
Module Module1
Sub Main()
Dim samples As String() = {
"ABC-123",
"Café",
"こんにちは",
"😀",
"ABC",
"ABC" & vbLf & "123"
}
For Each s In samples
Dim byUtf8 As Boolean = (Encoding.UTF8.GetByteCount(s) <> s.Length)
Dim byScan As Boolean = ContainsNonAscii_ByCharScan(s)
Console.WriteLine("[" & s & "] Utf8Compare=" & byUtf8 & " Scan=" & byScan)
Next
End Sub
Public Function ContainsNonAscii_ByCharScan(ByVal s As String) As Boolean
If String.IsNullOrEmpty(s) Then
Return False
End If
For Each ch As Char In s
If ch > ChrW(&H7F) Then
Return True
End If
Next
Return False
End Function
End Module
「ASCIIのみ」の要件に対しては、Utf8CompareとScanが同じ結果になることが多いはずです。差が出る場合は、入力に制御文字が含まれている、あるいは要件が「ASCII」ではなく「表示可能ASCII」や「特定コードページで表現可能」になっている可能性を疑うのが近道です。
入力チェックを強くしすぎる前に考えるべきこと
ASCII制限はシンプルで強力ですが、UXや国際化要件と衝突しやすいのも事実です。例えば、氏名や住所、自由記述欄までASCII縛りにすると、現場で回避入力(ローマ字化や記号乱用)が増え、結局データ品質が落ちることもあります。
- 本当にASCIIである必要があるのはどの項目か(ID、外部連携キー、ファイル名など)
- 「非ASCIIを許容するが、危険文字だけ弾く」方が良い項目はどれか
- 保存はUnicodeで行い、連携時にエンコード変換・置換ルールを設ける方が安全ではないか
判定ロジック自体の正しさだけでなく、「なぜその制限が必要か」もセットで設計すると、運用トラブルが減ります。
まとめ:安全性の要点だけ押さえて選ぶ
Encoding.UTF8.GetByteCount(s) <> s.Lengthは、非ASCII文字の有無チェックとして基本的に成立します。- 例外対策として、
Nothing(null)チェックは必須です。 - 保守性と性能の観点では、
For Eachでch > 0x7Fを確認する方法が意図が明確で実務向きです。 - 「ASCIIのみ」ではなく「表示可能ASCIIのみ」や「Shift-JISで表現可能」など、要件が少しでも違うと判定方法も変わるため、言葉の定義を先に固めるのが成功の近道です。

コメント