3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VB6からC#への移植でハマりやすい罠と、テスト可能な設計への道

3
Posted at

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コンテナ、テスト、インターフェース)を活かせる構造に変えて初めて、保守性・検証可能性・拡張性が手に入ります。

移植後のリファクタリングこそが本当の移植だ、と言えるかもしれません。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?