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


コメント