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公開予定)—移植して動き始めた業務画面の挙動を整える時に効く

以上!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次