VB.NETで非ASCII文字を判定する方法:UTF-8のGetByteCountとString.Length比較の安全性と落とし穴

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)
ASCIIA, 0, -, 空白, 改行U+0000〜U+007F11一致しやすい
アクセント付きなどé, üU+0080〜U+07FF21不一致になる
日本語など(BMP内)あ, 漢, 語U+0800〜U+FFFF31不一致になる
絵文字など(サロゲート)😀U+10000以上42不一致になる

この関係が崩れない限り、提示式は「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改行・タブもOKch <= 0x7F
表示可能ASCIIのみ0x20〜0x7E英数字・記号のみ、改行NG0x20 <= 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で表現可能」など、要件が少しでも違うと判定方法も変わるため、言葉の定義を先に固めるのが成功の近道です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次