VB.net の Right / Mid / Left を C# に翻訳する完全早見表

VB.net の Right / Mid / Left を C# に翻訳する完全早見表

VB.net の既存資産を C# に移植する案件で、最初の地雷はだいたい文字列関数 だ。Right(s, 3) を「右から3文字でしょ?」と s.Substring(s.Length - 3) に直接置き換えて、後で「文字数より大きい長さを渡したら例外で落ちた」と本番障害になる。1ベースと0ベースの違いも、Surrogate Pair の挙動も、VB.net 側で勝手に面倒を見てくれていた部分が C# では全部むき出しになる。

俺も .NET Framework 4.7.2 で動いていた VB.net の業務系を C# に移植する案件で、Right / Mid / Left の置換だけで半日溶かしたことがある。今回は VB.net の Right / Mid / Left を C# に翻訳する時に踏むハマりどころと、コピペで動く書き換えヘルパ を早見表でまとめておく。

目次

結論:直訳禁止、ヘルパを1個作って通す

先に答えを置く。VB.net の文字列3関数を C# に翻訳する時のルールは次の3つ。

  1. 直訳しないs.Substring(s.Length - n) を裸で書かない)
  2. 長さオーバーフロー・null・Surrogate Pair を安全に握り潰す薄いヘルパ を1個用意する
  3. Microsoft.VisualBasic 参照は最終手段(移植が終わるまでの暫定としてのみ)

VB.net の Right/Mid/Left は中で boundary check や型変換を勝手にやってくれるので、ナイーブな直訳だと半分は動くが残り半分で例外が飛ぶ。Substring のラッパーを最初に書いて、移植は全部そこを通すのが一番事故が少ない。

VB.net Right/Mid/Left → C# ヘルパ層への対応フロー

なぜ直訳は危険か:VB.net と C# の文字列インデックス仕様差

項目 VB.net Mid(s, start, len) C# s.Substring(start, len)
起点 1ベース(最初の文字が 1 0ベース(最初の文字が 0
長さオーバーフロー 残り全部を返す(例外なし) ArgumentOutOfRangeException
Nothing / null 入力 空文字を返す NullReferenceException
全角文字 char 単位で動く(Surrogate Pair は分裂しうる) 同上
マイナス長 ArgumentException ArgumentOutOfRangeException

つまり VB.net 側は「失敗しないように勝手に処理してくれる」コードに対して、C# は「指定が雑ならその場で落ちる」設計になっている。Right(s, 100) が VB.net で素通りしていた業務ロジックを s.Substring(s.Length - 100) で直訳すると、データの内容次第で本番障害が起きる。

Right の仕様差

' VB.net
Dim r As String = Microsoft.VisualBasic.Strings.Right("abcde", 3)  ' "cde"
Dim r2 As String = Microsoft.VisualBasic.Strings.Right("ab", 5)    ' "ab"(残り全部)
Dim r3 As String = Microsoft.VisualBasic.Strings.Right(Nothing, 3) ' ""(空文字)

これを C# で素直に直訳するとこうなる。

// NGパターン:直訳
string r = s.Substring(s.Length - 3);                // "cde" だが s が短いと例外
string r2 = "ab".Substring("ab".Length - 5);         // ArgumentOutOfRangeException
string r3 = ((string)null).Substring(((string)null).Length - 3); // NRE

VB.net 時代に「とりあえず動く」で書かれていた箇所は、データの長さが短い時のことを考えていない可能性が高い。C# 移植では 長さチェックを書き手側で明示的に入れる ことになる。

Mid の仕様差

' VB.net
Dim m As String = Microsoft.VisualBasic.Strings.Mid("abcdef", 2, 3)  ' "bcd"
Dim m2 As String = Microsoft.VisualBasic.Strings.Mid("abc", 5, 2)    ' ""(範囲外でも例外なし)

C# の Substring は範囲外を渡すと即例外。VB.net の Mid は1ベース、Substring は0ベース。起点を直して、なおかつ範囲外を握り潰す という2段階の翻訳が要る。

Left の仕様差

' VB.net
Dim l As String = Microsoft.VisualBasic.Strings.Left("abcde", 3)   ' "abc"
Dim l2 As String = Microsoft.VisualBasic.Strings.Left("ab", 5)     ' "ab"

Left は VB.net で一番直訳が通りやすいが、長さオーバーフローでまた ArgumentOutOfRangeException を踏む。やはりラッパーを1個通すのが安全。

最短対処:C# 用のヘルパメソッド3点(コピペで動く)

ここから本題。3つの関数に対応するヘルパを置いていく。全部 .NET Framework 4.7.2 / VS2019 で動作確認済み の書き方。

ヘルパ1: Right の C# 実装

public static class StringCompat
{
    /// <summary>VB.net の Right 相当。null・長さオーバーフロー・負値を吸収。</summary>
    public static string Right(string s, int length)
    {
        if (string.IsNullOrEmpty(s) || length <= 0) return string.Empty;
        if (length >= s.Length) return s;
        return s.Substring(s.Length - length);
    }
}

呼び出し側はこうなる。

string r1 = StringCompat.Right("abcde", 3);    // "cde"
string r2 = StringCompat.Right("ab", 5);       // "ab"
string r3 = StringCompat.Right(null, 3);       // ""

VB.net の挙動に合わせて、null → 空文字 / オーバーフロー → 残り全部 / 負値 → 空文字、を全部吸収している。

欠点: Surrogate Pair(𠮷野家の 𠮷 のような U+10000 以上の文字)を char 単位で扱うので、絵文字や一部漢字を含む文字列で末尾の char が壊れる可能性がある。これは VB.net の Right 自体が char 単位なので「同等の挙動」だが、現代の業務系で絵文字を扱うなら後述の「ハマりポイント」のサロゲート対応版を検討する。

VB.net Right と C# Substring + ヘルパの比較イメージ

ヘルパ2: Mid の C# 実装

public static class StringCompat
{
    /// <summary>VB.net の Mid 相当。1ベース起点、範囲外は空文字。</summary>
    public static string Mid(string s, int start, int length)
    {
        if (string.IsNullOrEmpty(s) || length <= 0) return string.Empty;
        // VB.net は 1 ベースなので 0 ベースに変換
        int startIndex = start - 1;
        if (startIndex < 0) startIndex = 0;
        if (startIndex >= s.Length) return string.Empty;

        int safeLen = Math.Min(length, s.Length - startIndex);
        return s.Substring(startIndex, safeLen);
    }

    /// <summary>VB.net の Mid(lengthなし)相当。指定位置から末尾まで。</summary>
    public static string Mid(string s, int start)
    {
        if (string.IsNullOrEmpty(s)) return string.Empty;
        int startIndex = start - 1;
        if (startIndex < 0) startIndex = 0;
        if (startIndex >= s.Length) return string.Empty;
        return s.Substring(startIndex);
    }
}

Mid は3引数(start + length)と2引数(start のみ)の両方が VB.net で使われているので、C# 側もオーバーロードしておくのが楽。

欠点: 引数が3つになるので、移植元の VB.net コードで「Mid(s, n)」と「Mid(s, n, m)」が混ざっていると、置換時に手で引数の数を合わせる作業が増える。sed 等の機械的な一括置換は向かない。

ヘルパ3: Left の C# 実装

public static class StringCompat
{
    /// <summary>VB.net の Left 相当。null・長さオーバーフロー・負値を吸収。</summary>
    public static string Left(string s, int length)
    {
        if (string.IsNullOrEmpty(s) || length <= 0) return string.Empty;
        if (length >= s.Length) return s;
        return s.Substring(0, length);
    }
}

Right の鏡写しなので素直。これも Microsoft.VisualBasic.Strings.Left を素通りしていた挙動と一致するように書いてある。

欠点: シンプルすぎて拡張メソッド化したくなるが、string の拡張メソッドは衝突しやすい。チームで複数ライブラリを引いている場合、別人の Left() 拡張と被ると診断メッセージで初学者がハマるので、static class のメソッドのままにしておく のがチーム作業では事故が少ない。

抜け道:Microsoft.VisualBasic 参照を C# プロジェクトで使う

「ヘルパを書くのも面倒、Microsoft.VisualBasic を参照すれば VB.net の関数がそのまま使える」というのは事実。

// using Microsoft.VisualBasic; を入れて
string r = Strings.Right("abcde", 3);  // VB.net とまったく同じ挙動で動く

.NET Framework でも .NET 5+ でも Microsoft.VisualBasic.Core が利用可能で、Right/Mid/Left がそのまま呼べる。移植期間中の暫定対応 としては有効。

ただし 長期運用で残すと不利 になる理由が3つある。

  1. C# 開発者の認知コストが上がる:レビューで「なぜ VB の関数を C# から呼ぶのか」が議論になる
  2. NuGet の依存ツリーが汚れる.NET 5+ では Microsoft.VisualBasic.Core のパッケージ参照が必要になり、最小依存原則に反する
  3. 将来のフレームワーク更新で剥がしにくい:移植案件の終盤で「ここだけ VB 残ってますね」を毎回説明することになる

俺の現場では、移植開始から3ヶ月は Microsoft.VisualBasic を許容して、それ以降のフェーズで全部 StringCompat に置換するルールにした。移植中の暫定としては正解、長期で残すなら誤り という整理が落とし所だと思う。

欠点まとめ: 楽だが借金として残る。3ヶ月以内に剥がす計画とセットでなら使える。

ハマりポイント:早見表に入れにくい3つの罠

ここからは、上のヘルパを入れた で踏む典型ハマり。

全角文字・Surrogate Pair で末尾が壊れる

StringCompat.Right("𠮷野家", 1) は char 単位で末尾を切るので、Surrogate Pair の片割れだけが返って 不正な文字列 になることがある。これは VB.net の Right も同じ挙動だが、現代の業務系で人名・住所・商品名に絵文字や U+10000 以上の漢字が入るケースが増えていて、無視できない。

StringInfo を使うと書記素単位(人間が見る1文字単位)で扱える。

public static string RightByGrapheme(string s, int length)
{
    if (string.IsNullOrEmpty(s) || length <= 0) return string.Empty;
    var si = new System.Globalization.StringInfo(s);
    if (length >= si.LengthInTextElements) return s;
    int startElement = si.LengthInTextElements - length;
    return si.SubstringByTextElements(startElement, length);
}

業務系で「人名フィールドの末尾1文字を取って当社識別コードを付ける」みたいな処理がある場合、char 単位 vs 書記素単位を意識しないと、本番でレアケースの障害が出る。

null 入力時の例外と空文字の境界

VB.net 側で Nothing を渡すと空文字が返るのが暗黙の前提になっているコードを移植すると、C# で null を渡した時に NRE で落ちる。ヘルパで string.IsNullOrEmpty ガードを入れているのはこのため。移植元のコードを読む時は、Nothing チェックなしで Right/Mid/Left に渡している箇所を全部洗い出す のが事前作業として効く。

// 安全な書き方(null 入力許容)
string suffix = StringCompat.Right(record.LastName, 2);  // record.LastName が null でも OK

文字数 vs バイト数の混乱

Mid(s, 3, 2) を「3バイト目から2バイト分」と思って書いている VB.net コードに稀に出会う。VB.net の Mid文字数 ベースで動くので、SJIS バイト列を扱いたいなら別途 Encoding.GetBytes で変換する必要がある。

// バイト単位で取りたい場合(SJIS で固定長レコードを切る等)
byte[] bytes = System.Text.Encoding.GetEncoding("Shift_JIS").GetBytes(s);
byte[] slice = new byte[len];
Array.Copy(bytes, start, slice, 0, len);
string result = System.Text.Encoding.GetEncoding("Shift_JIS").GetString(slice);

業務系で固定長レコードを扱う現場なら、Right/Mid/Left の翻訳と合わせてバイト指向の関数も別ヘルパに切り出すと、後の保守が楽になる。

現場メモ:移植案件は「枯れた知識」が金になる

ここから少し本題から離れる。

VB.net → C# の移植は、SNS で出てくるモダン .NET の話題からすると地味な仕事に見える。同期は React や TypeScript で SaaS のフロントを書いている、らしい。家族との生活もあって、いきなりキャリアを切り替える話でもないので、温度感を上げすぎず付き合うのが現実的。読んでいるあなたの現場でも、VB.net の保守コードがまだ動いている画面が3〜4割は残っているはず。

ただ、移植案件は 「枯れた知識」がそのまま単価に乗る 領域でもある。今日書いた1ベース vs 0ベース、Substring の長さオーバーフロー、Surrogate Pair の挙動の話は、2002年(VB.NET 初版)から仕様が変わっていない。今後10年で大きく変わる気配もない。20年動く知識が、月単価60万〜80万のクラスで普通に売れる。

C# が完全な型言語である強みも、ここで生きる。VB.net で書かれていた「Option Strict Off で暗黙変換に頼っていたコード」を C# に移すと、コンパイル時点で型の弱さが全部洗い出される。VB.net の Mid を C# の string メソッドに置き換える時に、Nullable<int> の HasValue チェックや is null パターンを追加することになる。この作業は型システムの感覚をひたすら鍛えてくれる。

C# で身についた null 安全性・境界値チェック・型変換の感覚 は、TypeScript(strict モードでの string | null の扱い)や Java(Optional<T> のパターン)にそのまま持っていける。VB.net 移植は表面上は古い技術の保守だが、中身は 型言語の素養を毎日鍛える筋トレ になっている。俺自身、VB.net 移植で身についた「型の境界線を意識する癖」が、後の TypeScript 案件でそのまま単価交渉の根拠になった。フリーランス1案件目の月単価60万から、4案件目で月単価88万まで上がった転換点も、運や才能の話ではなく 「他言語に移植しても同じ事故を再現させない設計判断ができる」を言語化して見せた結果 だった。やっていたのは今日書いたような Right/Mid/Left の境界値チェックを言葉にする作業の延長で、再現は地味に可能なやつ。

「VB.net の保守はもう古い」と言われると気が滅入るが、コンパイラに守られない言語から守られる言語へ翻訳できる人 は、AI が書いたコードの境界線も読める人。2028年以降、AI が VB.net を C# に変換した出力を人がレビューする案件はすでに出始めていて、今後さらに増える蓋然性が高い。その時に強いのは、両方の仕様を肌で知っている人。

まとめ

  • VB.net の Right / Mid / Left は C# の Substring への直訳禁止。1ベース vs 0ベース、長さオーバーフロー、null の扱いが全部違う
  • 移植時は StringCompat.Right / Mid / Left の薄いヘルパを1個書いて、置換は全部ここを通す
  • Microsoft.VisualBasic 参照は移植期間中の暫定としてのみ有効。3ヶ月以内に剥がす計画とセットで使う
  • 全角文字を扱う現場では StringInfo.SubstringByTextElements で書記素単位対応版も用意する
  • バイト単位で切りたい固定長レコード処理は別ヘルパに切り出す

ここまで覚えておけば、VB.net 移植案件のオファーが来た時に「文字列処理の置換だけで何日かかる?」の見積もりが即答できる。

よくある質問

Q1. Microsoft.VisualBasic.Strings をそのまま使い続けるのは本当にダメ?

ダメではない。短期で剥がす計画があるなら問題ない。長期残すと、新規 C# 開発者の onboarding コストと将来の依存剥がしコストが積む、という問題があるだけ。判断は チームの平均寿命とプロジェクトの想定運用年数 で決める。

Q2. 拡張メソッド s.Right(3) の形にしないのはなぜ?

string の拡張メソッドは衝突しやすく、別ライブラリの Right 拡張と被るとビルドが通らない or 意図しない方が呼ばれる。チームで複数ライブラリを引いているなら、static class メソッドのまま StringCompat.Right(s, 3) の形にしておくほうが事故が少ない。

Q3. .NET 5+ ではどうする?

ほぼ同じ。Microsoft.VisualBasic.Core を NuGet 参照すれば Strings.Right が使える。ただし .NET 5+string には s[^3..] のような range/index 構文 が使えるので、Right(s, 3) の中身は return s[^Math.Min(length, s.Length)..]; で書ける。短く書けるが、引き続き null/ 負値ガードは必要。

Q4. パフォーマンスは Substring 直接呼びとヘルパ経由でどれくらい違う?

StringCompat.Right の本体は1〜2回の if と1回の Substring 呼びなので、JIT 後はほぼ Substring 直接と同等。マイクロベンチで差は見えない。設計判断で迷ったら可読性と安全性を優先 で問題ない。

Q5. VB.net の InStr / Replace / Trim も同じ問題がある?

InStr は1ベース返り、IndexOf は0ベース。Replace は VB.net 側が大文字小文字の扱いに Compare 引数を取れる。Trim は概ね同じ。Right/Mid/Left ほど致命的ではないが、起点と例外時挙動の違いは同じパターン で発生する。StringCompat を作るなら一式そろえるのがおすすめ。

関連記事

  • C# DataAdapter.Update() で DBNull 例外が出た時の最短対処(同日 slot 2 公開予定)— 移植直後の本番障害で詰まる前にこちらから読むと早い
  • C# DataGridView の DataSource を後から変更する全パターン(同日 slot 0 公開予定)— 移植して動き始めた業務画面の挙動を整える時に効く

以上!

この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次