VB6からC#への移植でハマりやすい罠と、テスト可能な設計への道
はじめに
「動いているVB6のコードをC#に移植した」——そういう案件は今も一定数あります。
しかし移植後のコードを見ると、文法はC#なのに、設計思想はVB6のままというケースが少なくありません。
本記事では実際にありがちな移植の落とし穴を整理し、C#らしい設計に近づけるための考え方を紹介します。
よくある移植アンチパターン
❌ パターン1:Formクラスの中にロジックを書く
VB6はFormにコードを書くスタイルが一般的でした。移植者がそのまま踏襲すると、C#でも同じ構造になります。
// ❌ 移植直後によくある状態
public partial class MainForm : Form
{
private void btnCalc_Click(object sender, EventArgs e)
{
// UIと計算ロジックが混在している
double raw = double.Parse(txtInput.Text);
double result = Math.Round(raw * 1.08, 2); // 消費税計算?
lblResult.Text = result.ToString();
}
}
これの何が問題かというと:
- UIを起動しないとロジックが動かない
- 実機(オシロ等)がないとデバッグできない
- VB6と同じ計算結果になっているか確認しようがない
テストを書きたくても、Formを起動してボタンを押さないと計算が走らない、という状況になります。
❌ パターン2:全部Staticクラスにする
C++やVB6のグローバル関数に慣れた開発者が書くとこうなりがちです。
// ❌ 全部Staticにしてしまった状態
public static class Calculator
{
public static double CalcTax(double price)
{
return Math.Round(price * 1.08, 2);
}
}
Staticクラス自体が悪いわけではありません。しかし状態を持つロジックや、外部依存のあるものをStaticにすると:
- インターフェースを実装できない → DIコンテナで差し替えられない
- モック化できない → テストが書けない
- スレッド安全でない状態変数が紛れ込みやすい
という問題が生じます。
🕳️ 特に危険な罠:丸め処理は「VB6で何を使っていたか」で変わる
移植で最も気づきにくいバグのひとつが、丸め処理の違いです。
ただしこれは単純に「VB6とC#で違う」という話ではなく、VB6側でどの方法を使っていたかによって結果が変わります。
VB6の丸め処理は2種類ある
VB6には丸めの方法が実は複数存在し、挙動が異なります。
' ① Round() 関数 → 銀行家の丸め(偶数への丸め)
Round(0.5) ' → 0 ← 偶数側
Round(1.5) ' → 2 ← 偶数側
Round(2.5) ' → 2 ← 偶数側
' ② Format() 関数 → 一般的な四捨五入(.5は切り上げ)
Format(0.5, "0") ' → "1"
Format(1.5, "0") ' → "2"
Format(2.5, "0") ' → "3"
' ③ Int(x + 0.5) の慣用パターン → 四捨五入
Int(2.5 + 0.5) ' → 3
VB6の Round() が銀行家の丸めをすることへの不満から、
Format() や Int(x + 0.5) で代替するコードがよく書かれていました。
C#の Math.Round のデフォルト
C#の Math.Round() はデフォルトで銀行家の丸めです。
これはVB6の Round() と同じ挙動です。
Math.Round(0.5) // → 0(偶数への丸め)
Math.Round(1.5) // → 2
Math.Round(2.5) // → 2(偶数への丸め)
移植でズレが生じるパターン
問題はVB6側で Format() や Int(x + 0.5) を使っていたケースです。
' VB6:四捨五入したくてこう書いていた
Format(2.5, "0") ' → "3"
Int(2.5 + 0.5) ' → 3
// C#に移植したときに Math.Round を使うと結果が違う!
Math.Round(2.5) // → 2 ← !!!
つまり**「VB6でどの関数を使っていたか確認せずに Math.Round に置き換えると、結果がズレる可能性がある」**わけです。
計測データを大量に処理するシステムでは、この差が積み重なって無視できない誤差になりえます。
移植前に確認すべきこと・修正方法
まずVB6側のコードを確認し、何を使っているかを把握します。
' VB6側がこのパターンなら要注意
Format(x, "0.00") ' 四捨五入
Int(x + 0.5) ' 四捨五入
C#側では MidpointRounding.AwayFromZero を明示して挙動を合わせます。
// ✅ 四捨五入(VB6のFormat/Int+0.5パターンに合わせる場合)
Math.Round(2.5, MidpointRounding.AwayFromZero) // → 3
Math.Round(2.345, 2, MidpointRounding.AwayFromZero) // → 2.35
// ✅ VB6のRound()と合わせる場合はデフォルトのままでOK
Math.Round(2.5) // → 2(どちらも銀行家の丸め)
VB6側で Round() を使っていたのか、Format() を使っていたのかを
移植前に必ず確認することが重要です。
✅ テスト可能な設計に近づけるリファクタリング
Step 1:ロジックをFormから引き剥がす
// ✅ 計算ロジックを独立したクラスに分離する
public interface ISensorCalculator
{
double CalcWithTax(double price);
}
public class SensorCalculator : ISensorCalculator
{
public double CalcWithTax(double price)
{
// Math.Round の第三引数を明示してVB6と挙動を合わせる
return Math.Round(price * 1.08, 2, MidpointRounding.AwayFromZero);
}
}
// Formはロジックを持たず、UIの制御だけに専念する
public partial class MainForm : Form
{
private readonly ISensorCalculator _calculator;
public MainForm(ISensorCalculator calculator)
{
InitializeComponent();
_calculator = calculator;
}
private void btnCalc_Click(object sender, EventArgs e)
{
double raw = double.Parse(txtInput.Text);
lblResult.Text = _calculator.CalcWithTax(raw).ToString();
}
}
Step 2:テストを書けるようになる
[TestClass]
public class SensorCalculatorTest
{
private readonly ISensorCalculator _calc = new SensorCalculator();
[TestMethod]
[DataRow(100.0, 108.0)]
[DataRow(1000.0, 1080.0)]
[DataRow(0.5, 0.54)] // 丸めが絡むケースも検証できる
public void CalcWithTax_VB6と同じ結果になること(double input, double expected)
{
var result = _calc.CalcWithTax(input);
Assert.AreEqual(expected, result);
}
}
これでUIも実機も不要で、VB6と計算結果が一致するか机上で確認できる環境が整います。
Step 3:データ取得部分も分離する(オシロ等の実機依存を切り離す)
// センサーからのデータ取得をインターフェースで抽象化
public interface ISensorDataSource
{
double[] GetRawData();
}
// 本番用:実際にオシロから取得する
public class OscilloscopeDataSource : ISensorDataSource
{
public double[] GetRawData() { /* 実機との通信 */ }
}
// テスト用:CSVやハードコードされたデータを返す
public class CsvDataSource : ISensorDataSource
{
private readonly string _path;
public CsvDataSource(string path) => _path = path;
public double[] GetRawData() => File.ReadAllLines(_path)
.Select(double.Parse).ToArray();
}
実機なしでテストデータ(CSVなど)を流し込んで処理結果を検証できるようになります。
まとめ
| 問題 | 原因 | 対策 |
|---|---|---|
| UIを起動しないと動かない | ロジックがFormの中にある | ロジックを独立クラスに分離 |
| テストが書けない | Staticクラスばかり | インターフェースを介したDI設計 |
| VB6と計算結果が違う | VB6側の丸め関数を確認せず移植した | VB6で何を使っていたか確認する。Format()やInt(x+0.5)で四捨五入していた場合は MidpointRounding.AwayFromZero を明示。Round()やCLng()ならMath.Round()のデフォルトのままでOK |
| 実機なしでデバッグできない | IO依存がロジックと混在 | データソースをインターフェースで抽象化 |
VB6からC#への移植は「動く」だけでは不十分です。
C#の設計の恩恵(DIコンテナ、テスト、インターフェース)を活かせる構造に変えて初めて、保守性・検証可能性・拡張性が手に入ります。
移植後のリファクタリングこそが本当の移植だ、と言えるかもしれません。