みなさんこんにちは!ヒロポンです!
なんか、、「前任者が辞めた現場でExcel VBAが30人の業務を回してた」みたいな話ほんま多くないですか?
VBAで動いてる業務改善ツール誰もメンテできない状態でぶら下がっててある日「これC#に移植してくれ」って急に降ってくるやつとセットですよね。地獄のリバースエンジニアリング開始。
いわゆる前任者が退職してExcel VBAで30人が業務を回してた現場のC# WinForms移行が降ってきたという案件。
ありますよね。多分あるはず。
ってことで、今回はそんなあなたに向けて、VBA → C#移行で我々が頻出で踏む10機能の翻訳早見表を、diff-csharpで並列展示します。
💡文字列関数のVB.net側(Right/Mid/Left → Substring)の翻訳は別記事 VB.netのRight / Mid / LeftをC#に翻訳する完全早見表でより詳しく書きました。
これは関連する話なので、興味がある人は別タブ開いて後で読んでくださいな。
今回はExcel VBA (Officeマクロ環境)視点で、Interop / Marshal.ReleaseComObjectまで含めた広い範囲を10機能で網羅します。
忙しいあなたのために!最初にまとめ!!!
- VBA → C#の翻訳は10機能押さえれば90%は移行できる
- 文字列・数値・日付の翻訳は言語仕様レベルで完結(Docker検証可能)
- Excel Interop (Range/Cells/Workbook)の翻訳はWindows + Office実機環境前提で、Marshal.ReleaseComObjectの解放忘れでExcel.exeプロセスが残る罠あり
- ハマり3つ: Variant型→ objectキャスト/マクロ自動記録は移植不可/ Marshal.ReleaseComObject解放忘れ
- Docker (verify-dotnet9)検証はC#翻訳後コードの言語仕様レベルのみ。Excel Interop本体はWindows + Office実機で別途確認してください
以上!!!!!
10機能対応表—まず全体を1枚で
10機能のVBA → C#対応を、インデックス基点/ Variant型扱い/ Interop必要性の3軸で並べたのが下の表です。

💡上のPNGは視覚的な俯瞰用。「VBA Right C#」「VBA Mid翻訳」等の具体文字列で検索した時のSEOマッチ用に、同じ対応関係を下の表でも書きます。
| VBA関数・構文 | C#等価物 | インデックス基点 | 補足 |
|---|---|---|---|
| Range("A1")/ Cells(1,1) | Excel.Application経由Range | A1=1始まり/ Cells(行,列)=1始まり | Office Interop必須 |
| Right(s, 5)/ Left(s, 5) | s.Substring(s.Length-5)/ s.Substring(0, 5) | VBA 1始まり/ C# 0始まり | 言語仕様のみ |
| Mid(s, 8, 5) | s.Substring(7, 5) | VBA 8 → C# 7 (1ずれ) | 言語仕様のみ |
| IsEmpty(v) | string.IsNullOrEmpty(s)/ v == null | — | object型確定必須 |
| Application.WorksheetFunction.Sum(rng) | Enumerable.Sum()/ Linq | Range → IEnumerable変換 | RangeはInterop経由 |
| Workbook.Open(path) | excelApp.Workbooks.Open(path) | — | Marshal.ReleaseComObject必須 |
| FormatNumber(123456, 0) | 123456.ToString("N0") | — | CultureInfoに注意 |
| DateDiff("d", d1, d2) | (d2 – d1).Days / .TotalDays | — | TimeSpanに確定 |
| InStr(s, sub) | s.IndexOf(sub) | VBA 1始まり/ C# 0始まり | 見つからない時0 vs -1 |
| Replace(s, "a", "b") | s.Replace("a", "b") | — | s nullチェック必須 |
| On Error Resume Next | try-catch / try-finally | — | 構造化例外モデル |
文字列関数の翻訳— Right/Left/Mid/InStr/Replace
我々が一番頻繁に踏む領域。
で、実走した結果も貼ります。

ポイント:
- VBAは1始まり、C#は0始まり。
Mid(s, 8, 6)をSubstring(7, 6)に1引くのを忘れると1文字ずれます InStrはVBAで「見つからない時0」、C# IndexOfは「見つからない時-1」。戻り値の判定ロジックも書き直し必要- 移行時はテストデータで両方走らせて差分を見るのが安全(こんな感じで両方の出力を1行ずつ並べると目視で確認しやすい)
ここからは現場メモ。
数年前、VBAマクロをC#に移植する案件で「移植したらSubstringの開始位置が1ずれてる」という相談が上がってきました。
私の最初の判断は「ロジック自体は間違ってないし、すぐ直る」。。。
なので動作確認の手を抜いて、本番に近いデータで1回走らせてみた。「これで完了!よっしゃーー次の機能行こう!」ってなってました。
結果1文字ずつズレた文字列が全レコードに混入。
夕方の業務締めまでに直さないとアカン。。帰宅予定が。。。背中に冷や汗。
慌ててMid(s, 8, 6) → Substring(7, 6)に書き直したら15分で済んだんですよね。マジで良かった。
ただ、レコード再生成で1時間溶かしたのと、業務側に頭下げに行った時間で信頼回復にもう半日。技術的な復旧時間より、そっちのほうがしんどかったです。
今だったら普通に1始まり/0始まりの違いを最初にコメントで貼っとくやん??って思いますよ?
でもね。。当時は1始まり/ 0始まりの違いは頭で分かってるから大丈夫っていう謎の自信があったんですよね。
これが罠の正体。
教訓は1個!!!!!!
1ずらしはdiffコミットしてから走らせる前にもう1回見直す!!!!。
数値・日付の翻訳— Sum/DateDiff/FormatNumber

ポイント:
WorksheetFunction.Sum(rng)はRangeをIEnumerableに変換する1段が必要。
Excel Interop経由ならrng.Cast<object>().Sum(o => Convert.ToInt32(o))のような型確定処理を間に挟む。
DateDiff("d", ...)はTimeSpanで書く。
.Days (整数)と.TotalDays (小数)の使い分けに注意。
FormatNumber(123456, 0)はC#で.ToString("N0")。
ただしCultureInfoによって区切り文字が変わるので、業務系で固定したいなら.ToString("N0", CultureInfo.InvariantCulture)推奨。
Excel Interopの翻訳— Range/Workbook.Open
ここがWindows + Office実機環境前提の領域。
Linux containerでは検証不可なので、動作確認はWindows VS + Officeインストール済PCで実施してください。

ポイント:
- C#側はusingステートメントが効かない (COMオブジェクトはIDisposableじゃない)
Marshal.ReleaseComObjectを末端(Range)から順に呼ぶのが鉄則- 最後に
GC.Collect()で強制GCを回さないとExcel.exeプロセスが残る
絶対にやらないで。。
エラー処理の翻訳— On Error Resume Next / IsEmpty

ポイント:
On Error Resume Nextは後続実行を続けるエラー無視モードで、VBA初心者あるあるの濫用パターン- C#ではtry-catchで明示的に握ることになるので、移植時に「ここは握って続行、ここは停止」の判断をVBAコードから読み取り直す必要あり
- IsEmpty(v)はC#では
string.IsNullOrEmpty(s)orv == nullで書くが、Variant型(object)のままだと型確定エラーが出るので、var s = rng.Value as string;のように明示キャストで受ける
詳細は別記事 C#例外処理の正解— try-catch-finally / using / Exceptionフィルタの使い分けでtry-catchの構造化例外モデルを解説しました。
別タブで開いて後で読んでくださいな。
ハマりポイント—そうじゃないケースが3つあります
ここまで「10機能押さえれば移行できる」みたいに書いてきましたが、そうじゃないケースが3つあります。
あなたのために特別に3つ全部共有します。
① Variant型→ objectキャストで型確定エラー
VBAのRange.ValueはVariant型で、何でも入る箱として書かれてる現場が多いです。
C#側でrng.Valueを受け取るとobject型で返ってくる。
そのままint / stringに代入しようとするとマジでInvalidCastExceptionで落ちる。
ここからは現場メモ。
数年前、VBA移植案件でn = Range("A1").ValueをC#でそのままint n = rng.Value;と書いた相談が上がってきました。
私の最初の判断は「これコンパイル通るやろ」。
なので動作確認もせずコミットして、PRを上げた。「これで完了!よっしゃーー次のタスク行こう!」ってなってました。
結果コンパイルエラー → 修正 → ランタイムInvalidCastの二段オチ。
レビュアーから「これ動かないですよ」と内線が鳴って画面の前で完全に固まりましたよ。
帰る予定が。。。1時間ハマって背中に冷や汗。
慌ててConvert.ToInt32(rng.Value)に書き直したら通った。マジで良かった。
今だったら普通にVBAの暗黙変換はC#では効かないって分かるやん??って思いますよ?
でもね。。当時はVBAの暗黙変換に頼りすぎてて、Variant = objectという頭でしか書いてなかったんですよね。
これもやらかしの原因。
学び!!! 明示キャスト+型チェックを入れる: int n = Convert.ToInt32(rng.Value); or if (rng.Value is double d) { ... }。
VBA移植時はVariant出現箇所をgrepで全部洗い出して型確定処理を入れるクセを付ける。
②マクロ自動記録は移植不可
Excelの「マクロ記録」ボタンで生成されたVBAコードを、そのままC#に翻訳しようとすると詰みます。
マクロ自動記録は操作の結果系列をVBAで書き出すだけ。
操作の意図情報 (なぜこの範囲を選んだのか・なぜこの関数を使ったのか)が完全に落ちる。
C#に翻訳しても「何が起きているか」が読めないコードになる。
ここからは現場メモ。
数年前、後輩のマクロ移行を引き取った時、800行の自動記録コードがいきなり手元に降ってきました。
私の最初の判断は「とりあえず1行ずつ翻訳していけば終わるやろ」。
なので朝から黙々と翻訳開始。「これで完了!よっしゃーー金曜の夜飲みに行こう!」って3日後の自分に期待してました。
結果なんと3日溶かした末に「何が起きてるか分からんコード」が完成。
しかも翻訳途中でExcelの操作意図が分からん箇所が10個以上出てきて、後輩に聞きに行くハメに。
金曜の飲み予定が。。。土曜出社まで延長。背中に冷や汗。
慌てて元のマクロを書いた人に意図を聞きに行ったら2時間で意図が起きました。マジで良かった。
今だったら普通に翻訳の前に意図を聞きに行くやん??って思いますよ?
でもね。。当時はコードがあるから翻訳から入れるという単純な式しか頭になかったんですよね。
まじでやらかした。
学び!!! 自動記録コードはC#に持っていかず、一度VBAで意図を再構築してからC#に翻訳する。
操作意図が分からない場合は、元のマクロ作成者に聞くか、Excel側で同じ業務を1回手作業で再現して意図を起こす。
③ Marshal.ReleaseComObject解放忘れでExcel.exeプロセス残留
Excel Interopでファイルを開いた後、通常のC#流儀でusingブロックや変数破棄に任せて書いてしまうケース。
これをやるとExcel.exeプロセスが残り続ける。
タスクマネージャを見るとEXCEL.EXEがなんと10個並んでて、メモリが食い尽くされる。
Windowsサービスで動かしてた場合は徐々にサーバが詰まる。
ヤバい!!!
ここからは現場メモ。
数年前、月末の集計バッチで、Marshal.ReleaseComObjectのRange部分を書き忘れた状態でリリースしてしまいました。
私の最初の判断は「ApplicationとWorkbookだけ解放してれば十分やろ」。
なのでRangeとWorksheetの解放を省略。「これで完了!よっしゃーー金曜の夜飲みに行こう!」ってなってました。
結果30件のExcelファイル処理でExcel.exeプロセスが30個残った。
朝3時に「サーバのメモリ警告」アラートで起こされて、タスクマネージャで全EXCELプロセスをkillする羽目に。
寝てる予定が。。。背中に冷や汗。
慌てて末端からReleaseComObjectを並べ直したら朝6時には収束しました。ギリギリ耐えた。(耐えてない)
技術的な復旧は3時間で済んだんですよね。めでたしめでたし。。となるわけなく。インフラ担当からの内線が鳴り止まなくて、信頼回復にもう半日。
今だったら普通に末端から全部解放する共通ヘルパを最初に書くやん??って思いますよ?
でもね。。当時はApplicationだけ解放すれば芋づる式に消えるという単純な式しか頭になかったんですよね。
これもやらかしの原因。
学び!!! Range → Worksheet → Workbook → Applicationの末端から順にMarshal.ReleaseComObject + null代入。
最後にGC.Collect()+ GC.WaitForPendingFinalizers()を呼ぶ。
これを共通ヘルパクラス化して、全Excel Interop処理で呼び出すルールにすると事故が消える。
次のステップ— OpenXML SDK / ClosedXML
Excel InteropはWindows + Officeインストール必須で、サーバ環境では使えません。
我々が次に押さえるべき選択肢が2つ:
- OpenXML SDK (Microsoft公式): .xlsxファイルをOffice無しで直接読み書き。低レベルAPI
- ClosedXML: OpenXML SDKのラッパー。直感的に書ける。NuGet
Install-Package ClosedXML
業務系の月次バッチをWindows Serviceとして動かす現場では、Interop → ClosedXML移行が定石。
ただ関数が限られるので、ピボットテーブルやマクロが残ってるExcelは依然Interopが必要なケースもあります。
まとめ
VBA → C#移行は10機能の対応表を1枚作っておくだけで、移行時間が半分に縮みます。
- 文字列・数値・日付の翻訳 →言語仕様レベル(Docker検証可)
- Excel Interopの翻訳 → Windows + Office実機環境前提・Marshal.ReleaseComObject必須
- ハマり3つ(Variant型/マクロ自動記録/ Marshal解放忘れ)を事前に押さえる
ぶっちゃけ、我々がVBA → C#移行案件で「いい感じに半分の時間で終わる」ためには、翻訳前に10機能対応表を手元に置いておくことがマジで効きます!!!
引き継ぎ朝の業務メールで「VBAマクロ500行をC#に」と来た時、「ん??10機能の早見表があれば、まずRight/Left/MidとSum/DateDiff/Replaceだけ翻訳して骨格組めるな」と即判断できれば、こんな感じで朝のコーヒーが冷める前にコード設計が走ります!!!!
よくある質問
Q1. VBAのマクロを自動記録した結果をC#に直接コピペできますか?
A. 出来ません。
マクロ自動記録は操作の結果系列をVBAで書き出すだけで、操作の意図 (なぜその範囲を選んだか・なぜその関数を使ったか)が落ちます。
C#に持っていく時はVBAコードを読んで意図を再構築する必要があります。
ハマりポイント②で詳しく書いた通り。
Q2. Excel Interopで起動したExcel.exeプロセスが残り続けるのはなぜですか?
A. RCW (Runtime Callable Wrapper)の参照カウントが残っているからです。
Excel.Application / Workbook / Worksheet / Rangeそれぞれに対してMarshal.ReleaseComObjectを呼び、参照をnullにしてからGC.Collect()を回すと確実に解放されます。
ハマりポイント③参照。
Q3. VBAのRight/Mid/LeftはC#のSubstringと何が違う?
A. インデックス基点が違います。
VBAは1始まり (Mid(s, 8, 5)は8文字目から)、C#は0始まり (s.Substring(7, 5)で同じ結果)。
移行時は1ずらしを忘れると1文字ずれた文字列を返します。
詳細は文字列関数の翻訳セクションでdiff-csharpで並べました。
関連記事
- VB.netのRight / Mid / LeftをC#に翻訳する完全早見表— VB.net側(Office外)の文字列関数翻訳はこちら。今回のVBA (Excelマクロ)とは別環境
- C#例外処理の正解— try-catch-finally / using / Exceptionフィルタの使い分け— VBAのOn Error Resume NextをC#に持っていく時の前提はこちら
ついでに別タブ開いて後で呼んでおいてくださいませ!
以上!


コメント