C# 構造体 バイナリーファイルの読み込みの件

タグの編集
投稿者 ケンケン  (社会人) 投稿日時 2022/10/18 16:50:14
C# バイナリーファイルの読み込みの繰り返しが上手く行かない。

   構造体の内容です。 

        [StructLayout(LayoutKind.Sequential)]    // フィールドを順にシリアライズ
        public struct usrData
        {

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string id;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string accID;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string name;

            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
            public string profileID;

            public long revDate;

            public int userLevel;

            public usrData(string ID, string AccID, string Name, string ProfileID, long RevData, int UserLevel)
            {

                id = ID;
                accID = AccID;
                name = Name;
                profileID = ProfileID;

                revDate = RevData;
                userLevel = UserLevel;
            }

        }

        ↓は実行処理

        private void button16_Click(object sender, EventArgs e)
        {
            string dataPath = Directory.GetCurrentDirectory() + @"\data\";

            FileStream fs = new FileStream(dataPath + "Mydata.data", FileMode.Open, FileAccess.Read);

            //ファイルのバイト長だけバイト配列を作成
            byte[] bs = new byte[fs.Length];

            for (; ; )
            {
                int readSize = fs.Read(bs, 0, bs.Length);

                Console.WriteLine("readSize :" + readSize);

                if (readSize == 0)
                {
                    break;
                }
                //ファイルの内容を読み込む
                fs.Read(bs, 0, bs.Length);

                Console.WriteLine("bs.length " + bs.Length);

                //ファイルが無い場合は終了する
                Form1.usrData Mydata;

                int size = bs.Length;
                IntPtr ptr = Marshal.AllocHGlobal(size);
                Marshal.Copy(bs, 0, ptr, size);
                Mydata = (Form1.usrData)Marshal.PtrToStructure(ptr, typeof(Form1.usrData));
                Marshal.FreeHGlobal(ptr);


                Console.WriteLine("id   :" + Mydata.id);
                Console.WriteLine("acc  :" + Mydata.accID);
                Console.WriteLine("name :" + Mydata.name);
                Console.WriteLine("prof :" + Mydata.profileID);
                Console.WriteLine("rev  :" + Mydata.revDate);
                Console.WriteLine("level:" + Mydata.userLevel);
            }

            fs.Close();

            Console.WriteLine("読み込み改終了");

        }

  ※ 3件のデータが有りますが、1件読んで終わってしまいます。
     繰り返して3件分のデータが表示されません。

    何方か、わかる方ご教授お願いいたします。
    
 
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2022/10/18 22:01:38
string を含む構造体では、 StructLayout に Charset も明示すべきです。
また、Pack も明示的に指定する必要があります。

// Marshal.SizeOf<usrData>() が返す構造体サイズ
[StructLayout(LayoutKind.Sequential)]  // 528
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 4)]  // 524
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 8)]   // 1040



> //ファイルのバイト長だけバイト配列を作成
> byte[] bs = new byte[fs.Length];
一括読み込みするのであれば、File.ReadAllBytes() では駄目でしょうか?
ReadAllBytes は、FileMode.Open, FileAccess.Read, FileShare.Read 相当の
FileStream 操作なので、コードがスッキリするかと思います。

fs.Read を繰り返す形にするのは、一括読み取りする場合では無く、
レコード単位など、ある程度のブロック単位に区切って順次読み取っていくような場面です。


> Mydata = (Form1.usrData)Marshal.PtrToStructure(ptr, typeof(Form1.usrData));
.NET Framework 4.5.1 以降では
 Mydata = Marshal.PtrToStructure<Form1.usrData>(ptr);
を使った方が良いですよ。


> ※ 3件のデータが有りますが、1件読んで終わってしまいます。
これは単純に、繰り返している箇所が無いからですね。

for + break; している箇所があるものの、
 「全部読みだす」→「ループ二回目では読み取り済みなので break」
という操作になっているだけで、データ件数 3 回分繰り返す実装にはなっていません。


ファイル全体のサイズは fs.Length だとして、
構造体一つの大きさは Marshal.SizeOf<usrData>() で取得できますので、
 (案1) ファイル長をレコード長で割った回数、繰り返し Copy する
 (案2) レコード長単位で Read しては Copy する処理を、Read した長さが不足するまで繰り返す
のいずれかにしてみては如何でしょうか。
投稿者 ケンケン  (社会人) 投稿日時 2022/10/19 15:37:03
ブロック読みに変えましたが、3件のデータが読みますが、一件目のみの内容しか表示されません。

 

            string dataPath = Directory.GetCurrentDirectory() + @"\data\";

            FileStream fs = new FileStream(dataPath + "Mydata.data", FileMode.Open, FileAccess.Read);

            int fileSize = (int)fs.Length;   // ファイルのサイズ
            byte[] buf = new byte[fileSize]; // データ格納用配列

            int readSize; // Readメソッドで読み込んだバイト数
            int remain = fileSize; // 読み込むべき残りのバイト数
            int bufPos = 0; // データ格納用配列内の追加位置

            Console.WriteLine("remain:" + remain);


            while (remain > 0)
            {

                // 528ytesずつ読み込む
                readSize = fs.Read(buf, bufPos, Math.Min(528, remain));

                Console.WriteLine("readSize:" + readSize);


                bufPos += readSize;
                remain -= readSize;

                Console.WriteLine("bufPos:" + bufPos);
                Console.WriteLine("remain:" + remain);

                Form1.usrData Mydata;

                int size = 528;
                IntPtr ptr = Marshal.AllocHGlobal(size);
                Marshal.Copy(buf, 0, ptr, size);
                Mydata = (Form1.usrData)Marshal.PtrToStructure(ptr, typeof(Form1.usrData));


                Console.WriteLine("id   :" + Mydata.id);
                Console.WriteLine("acc  :" + Mydata.accID);
                Console.WriteLine("name :" + Mydata.name);
                Console.WriteLine("prof :" + Mydata.profileID);
                Console.WriteLine("rev  :" + Mydata.revDate);
                Console.WriteLine("level:" + Mydata.userLevel);


            }
            fs.Dispose();
            fs.Close();

            Console.WriteLine("読み込み改終了");

        }

 どこを修正すれば良いですか?
 ご知恵をお願い致します。
 
投稿者 KOZ  (社会人) 投稿日時 2022/10/19 20:10:42
FileStream.Read メソッドの第2引数は、読み込んだデータを格納するバッファの位置を示します。

readSize = fs.Read(buf, bufPos, Math.Min(528, remain));

ここで buf の 読み込み位置をずらしているのに、

Marshal.Copy(buf, 0, ptr, size);

buf の先頭から変換しているので、毎回1件目が表示されることになります。

第2引数をゼロにすれば、とりあえず動きます。

あと修正すべきなのは

・528 と定数を使うのは良くない。

int structSize = Marshal.SizeOf(typeof(Form1.usrData));

として structSize を使う

・buf の長さは 528 バイトで良い

byte[] buf = new byte[structSize]; 

・remain は不要。 

readSize = fs.Read(buf, 0, structSize);

として、readSize = 0 ならループを抜ければよい。

ファイルの残りが structSize より小さい場合は buf にその分だけ読み込まれ、読み込んだサイズが readSize に入る
readSize が structSize より小さい値のとき、どうするのか仕様を決めること。

・ Marshal.AllocHGlobal に対する Marshal.FreeHGlobal が無い

IntPtr ptr = Marshal.AllocHGlobal(structSize);
try {

} finally {
    Marshal.FreeHGlobal(ptr);
}

のように使う。
また、毎回確保する必要は無いので try ブロックの中にループを入れてしまえば良い。

・ fs.Dispose() したら fs.Close() は不要。

投稿者 (削除されました)  () 投稿日時 2022/10/20 10:41:42
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2022/10/20 21:35:17
先の回答で「案1」と「案2」を設けましたが、手順的にはこのようなものです。

【案1】
 ファイル全体を一括で読み取った後で、そのバイナリをループ処理で
 切り出して構造体に変換していく手法。

① byte[] に全データを一括して取り込む。(バッファーサイズ = すべてのレコードの総量)
 最初の投稿にあった FileStream.Read でも良いですが、File.ReadAllBytes の方が処理としては楽かと。
 もしも FileStream.Read を使うなら、現状のように Marshal 処理の最中まで開き続けるのではなく
 全件 Read した時点で、ストリームを即時に閉じるべきです。

② byte[] の読み込み位置をずらしながら、1 レコードずつ Copy していきます。
 KOZ さんが書かれているように、読み込み開始位置は Marshal.Copy の第二引数で指定します。


byte[] buf = File.ReadAllBytes(filePath);           // ファイル全体を一括読み込みしてすぐにファイルを閉じる
int fileSize = buf.Length;                          // ファイルサイズ
int structSize = Marshal.SizeOf<Form1.usrData>();   // 構造体サイズ

IntPtr ptr = Marshal.AllocCoTaskMem(structSize);    // 単一レコード分のメモリを確保

Form1.usrData[] dataArray = new Form1.usrData[fileSize / structSize];  // 総レコード数を求めて配列を準備
for (int n = 0; n < dataArray.Length; n++)
{
    Marshal.Copy(buf, n * structSize, ptr, structSize);  // バッファ内のbuf[n]~buf[n+structSize-1] の範囲を転写
    dataArray[n] = Marshal.PtrToStructure<Form1.usrData>(ptr);  // 構造体として取得して順次配列に入れていく
}
Marshal.FreeHGlobal(ptr);   // メモリ解放

表示処理(dataArray);




【案2の場合】
 構造体と同じ大きさのメモリサイズを 1 ブロックとして、少しずつ読み取っていく手法。
 分割して読み取った分を順次、構造体に変換する処理を繰り返し行う。

① byte[] として確保するバッファーサイズは、ファイルサイズと同等では無く、
 構造体の 1 レコードと同等サイズとします。
 1 レコード分だけ Read しては Copy、また Read しては Copy を繰り返すようにする流れです。

② byte[] には現在の 1 レコード分しか無いので、この手法では
 Marshal.Copy の第二引数は常に 0 固定となります。


var dataList = new List<Form1.usrData>();
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
    int structSize = Marshal.SizeOf<Form1.usrData>();   // 構造体サイズ
    IntPtr ptr = Marshal.AllocCoTaskMem(structSize);    // 単一レコード分のメモリを確保
    byte[] buf = new byte[structSize];  // ファイル全体ではなく 1 レコード分だけのバッファ
    while (fs.Read(buf, 0, structSize) == structSize)   // バッファの buf[0]~buf[structSize-1] の位置に次のレコードを読み取る
    {
        Marshal.Copy(buf, 0, ptr, structSize);  // バッファ内のbuf[0]~buf[structSize-1] の範囲を転写
        dataList.Add(Marshal.PtrToStructure<Form1.usrData>(ptr));  // 構造体として取得して順次 List<> に入れていく
    }
    Marshal.FreeCoTaskMem(ptr);  // メモリ開放
}

表示処理(dataList);
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2022/10/20 21:43:25
> 3件のデータが有りますが、1件読んで終わってしまいます。
そのファイルの長さが 528 × 3 = 1584 バイトであることは確認済みでしょうか。

次のレコードの開始位置が何バイト目から開始されるのか、コーディング前に、
実際のファイルの構造をバイナリエディタ等で再確認しておくことをお奨めします。
構造体サイズとファイルサイズとで差が生じていた場合、処理内容によっては
データの読み取り開始位置のズレや不正なメモリアクセスの要因になりえます。


構造体サイズは、sizeof(Form1.userData) では拾えずに、
 Marshal.SizeOf<Form1.usrData>() … 先の私の回答
 Marshal.SizeOf(typeof(Form1.usrData)) … 先の KOZ さんの回答
などで取得できるわけですが、そこから得られたであろう 528 という値が
本来処理したいファイルに対して、適切なサイズであるかどうか確認しておきましょう。

今回のデータ構造はアライメントを意識したレイアウト設計にはなっていないようなので、
StructLayout で Pack (と Charset) を明示しておくことをお奨めします。

最初のコードだと、userLevel フィールドの後に、4 バイトのパディングが含まれますが、
たとえば、StructLayout で CharSet.Ansi, Pack = 1 を指定した場合は
そのパディングが削られ、528 バイトでは無く 524 バイトというサイズになります。


> readSize = fs.Read(buf, bufPos, Math.Min(528, remain));
Read メソッドに Math.Min を使っておられますが、
 Read(byte[] buffer, int offset, int count);
の count を変動させる必要は無く、固定値的に
「読み取りたいデータサイズ」つまり構造体サイズを指定するだけで十分です。

Read メソッドの戻り値が count と同じであれば、データを正しく読み取れたことになりますので、
バイナリを構造体へ転写した後、再度、次のブロックを Read するためにループ処理します。

一方、Read の戻り値が 0 になった場合は、既にファイル終端に到達していることを
意味しますので、そこでループを脱出するようにします。

そして Read の戻り値が 1 以上かつ count 未満であった場合には、
ファイルの残りの長さが構造体サイズに満たなかった事を意味します。
この場合、バッファーには読み取れた部分までが転写された状態になります。

Read されたデータサイズが構造体サイズに満たなかった場合に対する振る舞いとしては、
 「読み取れた部分まで処理できれば良いので、不足時は戻り値が 0 の時と同様に break; する」
 「不足時はファイル フォーマットの破損を意味するので、エラーメッセージを出して終了する」
 「足りないデータは初期値で置き換えるよう、データ補間して構造体に渡すように設計する」
などの選択肢が思い当たりますが、どう扱うかはプログラムの設計次第ですね。


> fs.Dispose();
> fs.Close();
FileStream に対して、両方を呼び出す必要はありません。どちらか一方で十分です。
迷った場合は、「using ブロックで囲む」もしくは「Close を呼び出す」のが良いでしょう。


仮に両方呼び出すにしても、Close を先に呼び、Dispose を後にすることをお奨めします。
Dispose 後に、そのオブジェクトの public なメソッドやプロパティを呼ぶことは、
さほど一般的ではありません。

というのも、オブジェクトによっては
「Dispose で処分した後に何か処理をさせようとすると、ObjectDisposedException 例外になる」
という振る舞いをするものがあるためです。

まぁ FileStream の場合は、Dispose 後に Close を呼んでも構わないのですが、
もしもあえて両方書くなら、Close → Dispose の方が望ましいでしょう。
(クラスによっては、Dispose せずに Close したオブジェクトを、後から再 Open 出来る設計がありえます)


なお、FileStream の内部実装的な話をすると:

Dispose() メソッドは、「Close() を呼び出す」だけの処理です。
https://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,251

Close() メソッドは、「Dispose(true)の呼び出し」後にファイナライズを抑制する処理です。
https://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,232

そして Dispose(bool) メソッドが 『後始末』をする処分処理の本体ですが、
これは public ではなく protected であり、外部からではなく内部的に呼ばれます。具体的には
 「ファイナライザから Dispose(false)される」
 「Close() から Dispose(true) される」
 「Dispose() から Close() されることで、Dispose(true) が呼ばれる」
といったルートで呼び出される設計です。
https://referencesource.microsoft.com/#mscorlib/system/io/filestream.cs,1270
投稿者 KOZ  (社会人) 投稿日時 2022/10/21 05:14:03
ふと思ったんですが、ReadFile API を呼び出したほうが楽な気もします。

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadFile(SafeHandle hFile, out Form1.usrData lpBuffer,
       int nNumberOfBytesToRead, out int lpNumberOfBytesRead, IntPtr lpOverlapped);

var lst = new List<Form1.usrData>();
int structSize = Marshal.SizeOf(typeof(Form1.usrData));
using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read)) {
    while (true) {
        if (ReadFile(fs.SafeFileHandle, out Form1.usrData data,
                            structSize, out int readedSize, IntPtr.Zero)) {
            if (readedSize < structSize) break;
            lst.Add(data);
        } else {
            throw new Win32Exception();
        }
    }
}

投稿者 ケンケン  (社会人) 投稿日時 2022/10/24 10:42:45
回答者のkozさん、魔界の仮面弁士さん

バイナリーファイルの技術サポートありがとうございます。

技術的な内容は、本当にためになります。

このサイトに巡り合えて本当に良かったです。

ReadFile API は自分のバージョンが違うので出来ませんでした。

他は、確認が取れました。

今後も、技術サポート宜しくお願い致します。