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つ。
- 直訳しない(
s.Substring(s.Length - n)を裸で書かない) - 長さオーバーフロー・null・Surrogate Pairを安全に握り潰す薄いヘルパを1個用意する
Microsoft.VisualBasic参照は最終手段(移植が終わるまでの暫定としてのみ)
VB.netのRight/Mid/Leftは中でboundary checkや型変換を勝手にやってくれるので、ナイーブな直訳だと半分は動くが残り半分で例外が飛ぶ。Substringのラッパーを最初に書いて、移植は全部そこを通すのが一番事故が少ない。

なぜ直訳は危険か: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単位なので「同等の挙動」だが、現代の業務系で絵文字を扱うなら後述の「ハマりポイント」のサロゲート対応版を検討する。

ヘルパ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つある。
- C#開発者の認知コストが上がる:レビューで「なぜVBの関数をC#から呼ぶのか」が議論になる
- NuGetの依存ツリーが汚れる:
.NET 5+ではMicrosoft.VisualBasic.Coreのパッケージ参照が必要になり、最小依存原則に反する - 将来のフレームワーク更新で剥がしにくい:移植案件の終盤で「ここだけ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 で経歴詳細を見る


コメント