PR

【C#】固定長のテキストファイルを構造体にマッピングする方法

C#

アプリ開発を行っていると、固定長のデータファイルを読み書きする機会というのが時折発生します。

データの長さが固定されているので、ちょっとプログラムが書ける人なら誰でも読み書きするロジックはゴリゴリ書けます。

が、経験を積んでしまうと後先考えてしまうんですよね。

私が対応した方法をご紹介します。

問題点

最初にも触れましたが、データの長さが固定されているので、ちょっとプログラムが書ける人ならロジックはゴリゴリ書けます。

例えば、以下のように1行ずつ読み込んだデータを固定サイズで取得していけばデータは取得できます。

StreamReader sr = null;
try
{
    sr = new StreamReader(ファイルパス);
    string line;
    while ((line = sr.ReadLine()) != null)
    {
        // 先頭から2文字分取得
        var data1 = line.Substring(0, 2);
        // 3文字目から5文字分取得
        var data2 = line.Substring(2, 5);
        // 8文字目から20文字分取得
        var data3 = line.Substring(7, 20);
    }
}
catch (Exception e)
{
    // 省略
}
finally
{
    if (sr != null)
    {
        sr.Close();
    }
}

しかし、この書き方だとデータの仕様が変わると大きく修正する必要が出てきます。

クライアント
クライアント

【仕様変更】
data2の前にdata1-2を追加することになりました。対応をお願いします。

さて、修正後のプログラムはどうなるでしょうか?

StreamReader sr = null;
try
{
    sr = new StreamReader(ファイルパス);
    string line;
    while ((line = sr.ReadLine()) != null)
    {
        // 先頭から2文字分取得
        var data1 = line.Substring(0, 2);

        var data1-2 = line.Substring(2, 4);  ← 仕様変更箇所

        // 3文字目から5文字分取得
        var data2 = line.Substring(2, 5);    ⇒ line.Substring(6, 5);
        // 8文字目から20文字分取得
        var data3 = line.Substring(7, 20);   ⇒ line.Substring(11, 20);
    }
}catch (Exception e)
{
    // 省略
}
finally
{
    if (sr != null)
    {
        sr.Close();
    }
}

データが追加されたことにより、追加データ以降の取得位置が変わるため、引きずられる形で修正が必要となりました。

開始位置を変数で加算していけば改善できますが、他に方法は無いのでしょうか?

var startPos = 0;
var data1 = line.Substring(startPos , 2);

startPos += 2;
var data1-2 = line.Substring(startPos, 4);

startPos += 4;
var data2 = line.Substring(startPos, 5);

startPos += 5;
var data3 = line.Substring(startPos, 20);

構造体にマッピングする

前置きが長くなりましたが調べた結果、以下にたどり着きました。

基本的には、上記サイトの記載内容そのままです。

追記した部分が2か所あります。
バイト配列への変換と構造体にキャストの2か所です。

以下、サンプルコードです。

while ((line = sr.ReadLine()) != null)
{
    // 構造体サイズを取得
    var structSize = Marshal.SizeOf(typeof(FixedWidthPosData));
    // 1行分の文字列をバイト配列に変換
    byte[] lineBytes = Encoding.UTF8.GetBytes(line);
    // アンマネージメモリにコピー
    var pData = Marshal.AllocHGlobal(structSize);
    Marshal.Copy(lineBytes, 0, pData, structSize);

    // 構造体にマッピング
    var structure = Marshal.PtrToStructure(pData, typeof(FixedWidthPosData));
    // アンマネージメモリ解放
    Marshal.FreeHGlobal(pData);

    // マッピングしたデータを構造体にキャスト
    FixedWidthPosData posData = (FixedWidthPosData)structure;

    // この後、読み込んだデータを処理するロジックを実装する
}

構造体はシンプルに以下のように定義しました。

[StructLayout(LayoutKind.Sequential)]
internal struct FixedWidthPosData
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public byte[] DataType;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public byte[] OutputKind;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] DataNumber;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] CreditDataNumber;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] SlipNumber;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
    public byte[] AgencyCode;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public byte[] Reserve1;
}

ポイントは、上から順番にマッピングされるということです。

[MarshalAs()] に記載している “SizeConst” でバイト数を指定しつつ、全てbyte[] で変数を定義することでマッピングの自由度を持たせることが出来ました。
項目の追加をする場合には、当該位置に項目を追加するだけです。削除も同様。

まとめ

読み込んだデータの格納用構造体(intやstringで定義し直した構造体)を別途用意しておいて、詰め替えるとさらに使い勝手はよくなります。

マッピング用構造体の順番だけ守っていれば、格納用構造体への詰め替えは特に制約はありません。

少しでもお役に立てば幸いです。

コメント

タイトルとURLをコピーしました