C#でバイナリを扱う方法|読み書き・変換・ファイル操作を初心者向けに解説

はじめに

C#でファイル操作や通信処理、画像・音声データの読み書きを行うときに避けて通れないのが「バイナリ」です。

普段よく扱うテキストファイルは、人間が読める文字として保存されています。一方で、画像ファイル、PDF、実行ファイル、独自形式のデータファイルなどは、多くの場合バイナリデータとして保存されています。

C#では、バイナリデータを主にbyte[]FileStreamBinaryReaderBinaryWriterなどを使って扱います。

この記事では、C#でバイナリを扱う基本から、ファイルの読み書き、データ型との変換、FileStreamの使い方、よくあるエラーと対処法まで、初心者にも分かりやすく解説します。

1. C#で扱う「バイナリ」とは

C#でバイナリを扱うとは、ファイルやメモリ上のデータを「バイト単位」で読み書きすることです。

バイナリは、文字列として読むのではなく、0x000xFFのようなバイト値の並びとして扱います。画像、音声、動画、圧縮ファイル、独自フォーマットのファイルなどは、基本的にバイナリデータです。

C#でバイナリを理解すると、単なるテキストファイル操作だけでなく、より低レベルなファイル操作やデータ変換ができるようになります。

1-1. バイナリデータとテキストデータの違い

テキストデータは、人間が読める文字を文字コードに従って保存したデータです。

例えば、次のような内容はテキストデータです。

Hello
こんにちは
12345

一方、バイナリデータは、人間がそのまま読める形式とは限らないバイト列です。

例えば、画像ファイルやPDFファイルをメモ帳で開くと、意味の分からない文字が大量に表示されることがあります。これは、バイナリデータを無理やり文字列として表示しているためです。

C#でテキストを扱う場合はstringStreamReaderを使うことが多いですが、バイナリを扱う場合はbyte[]FileStreamBinaryReaderなどを使います。

1-2. C#ではバイナリをbyte[]で扱うのが基本

C#でバイナリデータを扱うときの基本はbyte[]です。

byteは0から255までの値を持つ8ビットのデータ型です。バイナリデータは、このbyteが連続した配列として表現されます。

C#
byte[] data = { 0x01, 0x02, 0x03, 0xFF };

この例では、4バイトのバイナリデータを作成しています。

ファイルを読み込むときも、ネットワーク通信でデータを受け取るときも、最終的にはbyte[]として扱う場面が多くあります。

C#
byte[] fileData = File.ReadAllBytes("sample.bin");

このように、C#ではバイナリファイル全体をbyte[]として読み込むことができます。

1-3. バイナリファイルが使われる代表例

バイナリファイルは、さまざまな場面で使われています。

代表的な例としては、次のようなものがあります。

画像ファイルは、JPEG、PNG、GIFなどの形式で保存されます。これらは文字列ではなく、画像形式に従ったバイナリデータです。

音声ファイルや動画ファイルもバイナリデータです。MP3、WAV、MP4などは、専用の形式に従ってデータが格納されています。

PDFやExcelファイルも、内部的にはバイナリ形式で保存されることがあります。

また、アプリケーション独自の設定ファイルやセーブデータを、バイナリ形式で保存することもあります。テキスト形式よりもサイズを小さくできたり、読み書きを高速にできたりするためです。

1-4. 初心者が混乱しやすい「文字列」「数値」「バイト列」の違い

C#でバイナリを扱うときに初心者が混乱しやすいのが、「文字列」「数値」「バイト列」の違いです。

例えば、"123"という文字列と、123という数値は別物です。

C#
string text = "123";
int number = 123;

"123"は、文字1、文字2、文字3の並びです。一方、123は整数値です。

さらに、これらをバイナリとして保存すると、バイト列の内容も異なります。

C#
byte[] textBytes = Encoding.UTF8.GetBytes("123");
byte[] numberBytes = BitConverter.GetBytes(123);

textBytesは文字列"123"をUTF-8で変換したバイト列です。
numberBytesは整数123をメモリ上の表現に従って変換したバイト列です。

同じ「123」に見えても、文字列として扱うのか、数値として扱うのか、バイト列として扱うのかによって意味が変わります。

2. C#でバイナリファイルを読み込む方法

C#でバイナリファイルを読み込む方法はいくつかあります。

小さいファイルならFile.ReadAllBytesが簡単です。大きいファイルや一部だけ読みたい場合はFileStreamを使います。数値や文字列などの型を指定して読みたい場合はBinaryReaderが便利です。

2-1. File.ReadAllBytesでファイル全体をbyte[]に読み込む

最も簡単なバイナリファイルの読み込み方法は、File.ReadAllBytesを使う方法です。

C#
byte[] data = File.ReadAllBytes("sample.bin");

Console.WriteLine($"読み込んだバイト数: {data.Length}");

File.ReadAllBytesは、指定したファイルの内容をすべて読み込み、byte[]として返します。

画像ファイルを読み込む場合も同じです。

C#
byte[] imageData = File.ReadAllBytes("image.png");

Console.WriteLine($"画像ファイルのサイズ: {imageData.Length} bytes");

この方法は非常に簡単ですが、ファイル全体をメモリに読み込むため、大きなファイルには向いていません。

数KBから数MB程度のファイルであれば扱いやすいですが、数GBのファイルをFile.ReadAllBytesで読み込むと、メモリ不足になる可能性があります。

2-2. FileStreamで必要な分だけ読み込む

大きなファイルを扱う場合や、ファイルの一部だけを読みたい場合はFileStreamを使います。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[1024];
int bytesRead = fs.Read(buffer, 0, buffer.Length);

Console.WriteLine($"読み込んだバイト数: {bytesRead}");

この例では、sample.binから最大1024バイトを読み込んでいます。

FileStream.Readは、実際に読み込めたバイト数を返します。必ず指定したサイズ分だけ読み込めるとは限らないため、戻り値を確認することが重要です。

ファイル全体を少しずつ読み込む場合は、次のようにループします。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[4096];
int bytesRead;

while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
Console.WriteLine($"{bytesRead}バイト読み込みました");

// bufferの先頭からbytesReadバイト分を処理する
}

この方法なら、ファイル全体を一度にメモリへ読み込まずに処理できます。

2-3. BinaryReaderで型を指定して読み込む

バイナリファイルの中に、整数や浮動小数点数、文字列などが決まった順番で保存されている場合はBinaryReaderを使うと便利です。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

int id = reader.ReadInt32();
float score = reader.ReadSingle();
string name = reader.ReadString();

Console.WriteLine($"ID: {id}");
Console.WriteLine($"Score: {score}");
Console.WriteLine($"Name: {name}");

BinaryReaderを使うと、ReadInt32ReadSingleReadDoubleReadStringなどのメソッドで、型を指定してバイナリデータを読み込めます。

ただし、読み込む順番は書き込んだ順番と一致している必要があります。

例えば、書き込み時にintfloatstringの順番で保存したなら、読み込み時も同じ順番で読む必要があります。

2-4. 読み込み時にusingを使ってファイルを閉じ忘れないようにする

ファイルを扱うときは、読み込み後にファイルを閉じる必要があります。

C#では、usingを使うことで、処理が終わったときに自動的にリソースを解放できます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[1024];
int bytesRead = fs.Read(buffer, 0, buffer.Length);

古い書き方では、次のようにusingブロックを使います。

C#
using (FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[1024];
int bytesRead = fs.Read(buffer, 0, buffer.Length);
}

どちらの書き方でも、処理が終わるとFileStreamが自動的に閉じられます。

ファイルを閉じ忘れると、他の処理からそのファイルを開けなくなることがあります。C#でバイナリファイルを扱うときは、usingを使う習慣をつけましょう。

2-5. ファイルが存在しない場合の例外処理

存在しないファイルを読み込もうとすると、FileNotFoundExceptionが発生します。

C#
try
{
byte[] data = File.ReadAllBytes("notfound.bin");
Console.WriteLine($"読み込み成功: {data.Length} bytes");
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (IOException ex)
{
Console.WriteLine($"ファイル読み込みエラー: {ex.Message}");
}

事前にファイルの存在を確認することもできます。

C#
string path = "sample.bin";

if (File.Exists(path))
{
byte[] data = File.ReadAllBytes(path);
Console.WriteLine($"読み込んだバイト数: {data.Length}");
}
else
{
Console.WriteLine("ファイルが存在しません。");
}

ただし、File.Existsで確認した直後にファイルが削除される可能性もあります。そのため、実際の読み込み処理では例外処理も併用すると安全です。

3. C#でバイナリファイルを書き込む方法

C#でバイナリファイルを書き込む方法も複数あります。

byte[]をそのまま保存したい場合はFile.WriteAllBytesが簡単です。大きなデータを分割して書き込む場合はFileStreamを使います。型付きのデータを順番に書き込む場合はBinaryWriterが便利です。

3-1. File.WriteAllBytesでbyte[]をそのまま書き込む

byte[]をバイナリファイルとして保存するには、File.WriteAllBytesを使います。

C#
byte[] data = { 0x01, 0x02, 0x03, 0xFF };

File.WriteAllBytes("sample.bin", data);

Console.WriteLine("バイナリファイルを書き込みました。");

このコードを実行すると、sample.binというファイルに4バイトのデータが書き込まれます。

File.WriteAllBytesは、指定したファイルが存在しない場合は新規作成します。すでに存在する場合は上書きされます。

画像やPDFなどのコピーにも使えます。

C#
byte[] imageData = File.ReadAllBytes("source.png");
File.WriteAllBytes("copy.png", imageData);

この例では、画像ファイルをバイナリとして読み込み、そのまま別ファイルに保存しています。

3-2. FileStreamでバイナリデータを書き込む

FileStreamを使うと、バイナリデータを必要な分だけ書き込めます。

C#
byte[] data = { 10, 20, 30, 40, 50 };

using FileStream fs = new FileStream("sample.bin", FileMode.Create, FileAccess.Write);

fs.Write(data, 0, data.Length);

この例では、sample.binを新規作成し、byte[]の内容を書き込んでいます。

大きなデータを分割して書き込む場合も、FileStreamが適しています。

C#
using FileStream fs = new FileStream("large.bin", FileMode.Create, FileAccess.Write);

byte[] buffer = new byte[4096];

for (int i = 0; i < 10; i++)
{
// ここでは例として同じバッファを書き込む
fs.Write(buffer, 0, buffer.Length);
}

FileStreamは低レベルなファイル操作に向いており、読み込み位置や書き込み位置を細かく制御できます。

3-3. BinaryWriterでint・float・stringなどを書き込む

数値や文字列をバイナリ形式で書き込む場合は、BinaryWriterが便利です。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(100);
writer.Write(98.5f);
writer.Write("Taro");

このコードでは、intfloatstringを順番にバイナリファイルへ書き込んでいます。

読み込むときは、同じ順番でBinaryReaderを使います。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

int id = reader.ReadInt32();
float score = reader.ReadSingle();
string name = reader.ReadString();

Console.WriteLine($"{id}, {score}, {name}");

BinaryWriterBinaryReaderを組み合わせると、独自形式のバイナリファイルを簡単に作成できます。

3-4. 追記・上書き・新規作成をFileModeで使い分ける

FileStreamでは、FileModeを指定してファイルの開き方を制御できます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Create, FileAccess.Write);

よく使うFileModeには、次のようなものがあります。

FileMode.Createは、ファイルを新規作成します。すでに存在する場合は上書きします。

FileMode.CreateNewは、新しいファイルを作成します。すでに存在する場合は例外が発生します。

FileMode.Openは、既存のファイルを開きます。存在しない場合は例外が発生します。

FileMode.OpenOrCreateは、ファイルが存在すれば開き、存在しなければ新規作成します。

FileMode.Appendは、既存ファイルの末尾に追記します。存在しない場合は新規作成します。

例えば、既存のバイナリファイルの末尾にデータを追加する場合は、次のようにします。

C#
byte[] additionalData = { 0xAA, 0xBB, 0xCC };

using FileStream fs = new FileStream("sample.bin", FileMode.Append, FileAccess.Write);

fs.Write(additionalData, 0, additionalData.Length);

C#でバイナリファイルを書き込むときは、上書きしてよいのか、追記したいのか、新規作成だけにしたいのかを意識してFileModeを選ぶことが大切です。

3-5. 書き込み時のアクセス権限エラーへの対処

ファイルに書き込むとき、アクセス権限がない場所を指定するとUnauthorizedAccessExceptionが発生することがあります。

例えば、システムフォルダや他のユーザーのフォルダに書き込もうとすると、権限エラーになる場合があります。

C#
try
{
byte[] data = { 1, 2, 3 };
File.WriteAllBytes(@"C:\Windows\sample.bin", data);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ファイルに書き込む権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"書き込みエラー: {ex.Message}");
}

対処法としては、書き込み可能なフォルダを使うことが基本です。

ユーザーごとの保存先を使う場合は、次のようにEnvironment.GetFolderPathを使えます。

C#
string folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string path = Path.Combine(folder, "sample.bin");

byte[] data = { 1, 2, 3 };
File.WriteAllBytes(path, data);

デスクトップアプリや業務アプリでは、ユーザーが書き込み可能なフォルダを保存先に選ぶようにすると安全です。

4. byte[]と各データ型を変換する方法

C#でバイナリを扱うときは、byte[]と数値、文字列、Base64、16進数文字列などを相互変換する場面がよくあります。

変換方法を理解しておくと、ファイル保存、通信、暗号化、ハッシュ計算、画像処理などで役立ちます。

4-1. int・float・doubleなどの数値をbyte[]に変換する

数値をbyte[]に変換するには、BitConverter.GetBytesを使います。

C#
int number = 123456;
byte[] bytes = BitConverter.GetBytes(number);

Console.WriteLine(BitConverter.ToString(bytes));

floatdoubleも同じように変換できます。

C#
float f = 12.34f;
double d = 56.78;

byte[] floatBytes = BitConverter.GetBytes(f);
byte[] doubleBytes = BitConverter.GetBytes(d);

shortlonguintなどもBitConverter.GetBytesで変換できます。

C#
short s = 100;
long l = 1234567890L;

byte[] shortBytes = BitConverter.GetBytes(s);
byte[] longBytes = BitConverter.GetBytes(l);

ただし、BitConverterを使う場合はエンディアンに注意が必要です。エンディアンについては後の章で解説します。

4-2. byte[]を数値型に戻す

byte[]を数値型に戻すには、BitConverter.ToInt32BitConverter.ToSingleなどを使います。

C#
int number = 123456;
byte[] bytes = BitConverter.GetBytes(number);

int restored = BitConverter.ToInt32(bytes, 0);

Console.WriteLine(restored);

floatdoubleに戻す場合は、次のようにします。

C#
float f = 12.34f;
byte[] floatBytes = BitConverter.GetBytes(f);

float restoredFloat = BitConverter.ToSingle(floatBytes, 0);

Console.WriteLine(restoredFloat);

BitConverter.ToInt32(bytes, 0)の第2引数は、読み込み開始位置です。

例えば、1つのbyte[]に複数の値が入っている場合は、開始位置を変えて読み込みます。

C#
byte[] data = new byte[8];

Array.Copy(BitConverter.GetBytes(100), 0, data, 0, 4);
Array.Copy(BitConverter.GetBytes(200), 0, data, 4, 4);

int first = BitConverter.ToInt32(data, 0);
int second = BitConverter.ToInt32(data, 4);

Console.WriteLine(first);
Console.WriteLine(second);

数値型には必要なバイト数があります。intなら4バイト、doubleなら8バイトが必要です。バイト数が足りないと例外が発生するため注意しましょう。

4-3. 文字列をbyte[]に変換する

文字列をbyte[]に変換するには、文字コードを指定してEncoding.GetBytesを使います。

C#
using System.Text;

string text = "こんにちは";
byte[] bytes = Encoding.UTF8.GetBytes(text);

Console.WriteLine(BitConverter.ToString(bytes));

ここではUTF-8を指定しています。

Shift_JISを使いたい場合は、環境によって追加設定が必要になることがありますが、基本的には次のように文字コードを指定します。

C#
Encoding encoding = Encoding.UTF8;
byte[] bytes = encoding.GetBytes("Hello C#");

C#で文字列をバイナリに変換するときは、必ず文字コードを意識することが重要です。

同じ文字列でも、UTF-8、UTF-16、Shift_JISなど、文字コードが違うとバイト列も変わります。

4-4. byte[]を文字列に戻す

byte[]を文字列に戻すには、変換時と同じ文字コードを使ってEncoding.GetStringを呼び出します。

C#
using System.Text;

string text = "こんにちは";
byte[] bytes = Encoding.UTF8.GetBytes(text);

string restored = Encoding.UTF8.GetString(bytes);

Console.WriteLine(restored);

変換時と違う文字コードを使うと、文字化けすることがあります。

C#
byte[] bytes = Encoding.UTF8.GetBytes("こんにちは");

// UTF-8で作ったバイト列はUTF-8で戻す
string text = Encoding.UTF8.GetString(bytes);

バイナリデータが画像やPDFなどの場合、Encoding.GetStringで文字列に変換してはいけません。文字列に変換してから戻すと、データが壊れる可能性があります。

文字列として扱ってよいのは、もともと文字列として保存されたバイナリデータだけです。

4-5. Base64文字列とbyte[]を相互変換する

Base64は、バイナリデータを文字列として表現するための形式です。

メール、JSON、Web APIなどでバイナリデータを文字列として送るときによく使われます。

byte[]をBase64文字列に変換するには、Convert.ToBase64Stringを使います。

C#
byte[] data = { 1, 2, 3, 4, 5 };

string base64 = Convert.ToBase64String(data);

Console.WriteLine(base64);

Base64文字列をbyte[]に戻すには、Convert.FromBase64Stringを使います。

C#
string base64 = "AQIDBAU=";

byte[] data = Convert.FromBase64String(base64);

Console.WriteLine(BitConverter.ToString(data));

Base64は文字列として扱いやすい反面、元のバイナリデータよりサイズが大きくなります。そのため、大容量ファイルを扱う場合は用途に応じて使い分けましょう。

4-6. 16進数文字列とbyte[]を相互変換する

バイナリデータの中身を確認するときは、16進数文字列に変換すると見やすくなります。

.NET 5以降では、Convert.ToHexStringを使えます。

C#
byte[] data = { 0x0A, 0x1B, 0xFF };

string hex = Convert.ToHexString(data);

Console.WriteLine(hex);

出力は次のようになります。

0A1BFF

16進数文字列をbyte[]に戻すには、Convert.FromHexStringを使います。

C#
string hex = "0A1BFF";

byte[] data = Convert.FromHexString(hex);

Console.WriteLine(BitConverter.ToString(data));

古い環境では、BitConverter.ToStringを使って確認することもできます。

C#
byte[] data = { 0x0A, 0x1B, 0xFF };

string hex = BitConverter.ToString(data);

Console.WriteLine(hex);

この場合、出力は0A-1B-FFのようにハイフン区切りになります。

5. バイナリ操作で重要なFileStreamの基本

C#でバイナリファイルを本格的に扱うなら、FileStreamの理解は欠かせません。

File.ReadAllBytesFile.WriteAllBytesは簡単ですが、ファイル全体を一括で扱います。一方、FileStreamを使うと、ファイルの一部を読み込んだり、指定位置に移動したり、大きなファイルを少しずつ処理したりできます。

5-1. FileStreamの役割と使うべき場面

FileStreamは、ファイルに対してストリーム形式で読み書きするためのクラスです。

ストリームとは、データの流れを順番に読み書きする仕組みです。ファイル全体を一度に読み込むのではなく、必要な分だけ処理できます。

FileStreamを使うべき場面は、主に次のようなケースです。

大きなファイルを扱う場合、ファイル全体をメモリに読み込むと負荷が高くなります。FileStreamなら数KBずつ読み込んで処理できます。

ファイルの途中から読み書きしたい場合もFileStreamが便利です。Seekを使うことで、指定した位置に移動できます。

また、独自形式のバイナリファイルを作る場合にも、FileStreamを使うことで細かい制御ができます。

5-2. Read・Write・Seekの基本

FileStreamでよく使う基本メソッドは、ReadWriteSeekです。

Readは、ファイルからバイト列を読み込みます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[10];
int bytesRead = fs.Read(buffer, 0, buffer.Length);

Writeは、バイト列をファイルに書き込みます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Create, FileAccess.Write);

byte[] data = { 1, 2, 3, 4, 5 };
fs.Write(data, 0, data.Length);

Seekは、ファイル内の読み書き位置を移動します。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

fs.Seek(10, SeekOrigin.Begin);

byte[] buffer = new byte[5];
int bytesRead = fs.Read(buffer, 0, buffer.Length);

この例では、ファイルの先頭から10バイト目に移動し、そこから5バイト読み込んでいます。

5-3. ファイル内の指定位置から読み書きする方法

バイナリファイルでは、「先頭から何バイト目に何のデータがある」と決まっていることがあります。

そのような場合、Seekを使って指定位置に移動してから読み書きします。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

fs.Seek(4, SeekOrigin.Begin);

byte[] buffer = new byte[4];
int bytesRead = fs.Read(buffer, 0, buffer.Length);

int value = BitConverter.ToInt32(buffer, 0);

Console.WriteLine(value);

この例では、ファイルの先頭から4バイト目に移動し、そこから4バイトを読み込んでintに変換しています。

書き込みでも同じように指定位置へ移動できます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Write);

fs.Seek(8, SeekOrigin.Begin);

byte[] data = BitConverter.GetBytes(999);
fs.Write(data, 0, data.Length);

このコードでは、ファイルの先頭から8バイト目の位置に整数999を書き込んでいます。

ただし、既存ファイルの途中に書き込む場合は、その位置のデータが上書きされます。挿入されるわけではない点に注意しましょう。

5-4. 大きなファイルを分割して読み込む方法

大きなバイナリファイルを処理する場合は、バッファを使って分割読み込みします。

C#
string path = "large.bin";

using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[8192];
int bytesRead;
long totalRead = 0;

while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalRead += bytesRead;

// bufferの0からbytesReadまでを処理する
Console.WriteLine($"合計 {totalRead} バイト読み込みました");
}

この方法なら、ファイルサイズが大きくても、メモリ使用量を一定に保ちながら処理できます。

読み込んだbuffer全体を処理するのではなく、必ずbytesRead分だけ処理することが重要です。

最後の読み込みでは、バッファサイズより少ないバイト数になることがあるためです。

5-5. FileStreamとBinaryReader・BinaryWriterの違い

FileStreamは、バイト単位でファイルを読み書きするための基本的なクラスです。

一方、BinaryReaderBinaryWriterは、FileStreamなどのストリームの上にかぶせて使う便利なクラスです。

FileStreamだけを使う場合、数値を読み書きするには自分でbyte[]に変換する必要があります。

C#
int value = 123;
byte[] bytes = BitConverter.GetBytes(value);
fs.Write(bytes, 0, bytes.Length);

BinaryWriterを使うと、次のように直接書けます。

C#
writer.Write(123);

読み込みも同じです。

C#
int value = reader.ReadInt32();

つまり、細かくバイト単位で制御したい場合はFileStream、型付きで簡単に読み書きしたい場合はBinaryReaderBinaryWriterを使うと考えると分かりやすいです。

6. 実践例で学ぶC#のバイナリ読み書き

ここからは、C#でバイナリを読み書きする具体例を見ていきます。

基本的なbyte[]の保存から、画像ファイルの読み込み、ファイルコピー、独自フォーマットの作成まで、実践的なコードを紹介します。

6-1. byte[]を作成してバイナリファイルに保存する

まずは、byte[]を作成してバイナリファイルに保存してみましょう。

C#
byte[] data = new byte[]
{
0x48, 0x65, 0x6C, 0x6C, 0x6F
};

File.WriteAllBytes("hello.bin", data);

Console.WriteLine("hello.binを作成しました。");

このバイト列は、ASCII文字として見るとHelloに相当します。

ただし、バイナリファイルとして保存しているため、C#上では単なるbyte[]として扱っています。

6-2. 保存したバイナリファイルを読み込んで中身を確認する

保存したバイナリファイルを読み込んで、16進数で表示してみます。

C#
byte[] data = File.ReadAllBytes("hello.bin");

Console.WriteLine($"バイト数: {data.Length}");
Console.WriteLine(Convert.ToHexString(data));

出力例は次のようになります。

バイト数: 5
48656C6C6F

バイトごとに見やすく表示したい場合は、次のようにできます。

C#
foreach (byte b in data)
{
Console.WriteLine($"0x{b:X2}");
}

X2は、16進数2桁で表示するための書式指定です。

6-3. 画像ファイルをbyte[]として読み込む

画像ファイルも、C#ではbyte[]として読み込めます。

C#
byte[] imageData = File.ReadAllBytes("photo.jpg");

Console.WriteLine($"画像サイズ: {imageData.Length} bytes");
Console.WriteLine($"先頭バイト: {imageData[0]:X2}");

画像データを読み込んでも、C#上では単なるバイト列です。

画像として表示するには、画像処理用のライブラリやUIフレームワークで読み込む必要があります。

ただし、ファイルコピーやBase64変換、アップロード処理などでは、画像をbyte[]として扱うだけで十分な場合もあります。

C#
string base64 = Convert.ToBase64String(imageData);
Console.WriteLine(base64);

Web APIで画像を送る場合などに、Base64文字列へ変換することがあります。

6-4. 読み込んだバイナリを別ファイルにコピーする

バイナリファイルをコピーする最も簡単な方法は、読み込んだbyte[]をそのまま書き込むことです。

C#
byte[] data = File.ReadAllBytes("source.bin");
File.WriteAllBytes("copy.bin", data);

Console.WriteLine("コピーしました。");

小さなファイルであれば、この方法で問題ありません。

大きなファイルの場合は、FileStreamを使って分割コピーします。

C#
using FileStream source = new FileStream("source.bin", FileMode.Open, FileAccess.Read);
using FileStream destination = new FileStream("copy.bin", FileMode.Create, FileAccess.Write);

byte[] buffer = new byte[8192];
int bytesRead;

while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
{
destination.Write(buffer, 0, bytesRead);
}

Console.WriteLine("分割コピーが完了しました。");

この方法なら、大きなファイルでもメモリを大量に消費せずにコピーできます。

6-5. 独自フォーマットのバイナリファイルを作成する

C#では、BinaryWriterを使って独自フォーマットのバイナリファイルを作成できます。

例えば、次のような形式のファイルを作るとします。

先頭4バイト: 識別子
次の4バイト: バージョン番号
次の文字列: 名前
次の4バイト: スコア

コードは次のようになります。

C#
using FileStream fs = new FileStream("custom.dat", FileMode.Create, FileAccess.Write);
using BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8);

writer.Write(new byte[] { 0x43, 0x53, 0x42, 0x49 }); // "CSBI"
writer.Write(1); // バージョン
writer.Write("Taro"); // 名前
writer.Write(95); // スコア

読み込む側は、同じ順番で読みます。

C#
using FileStream fs = new FileStream("custom.dat", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs, Encoding.UTF8);

byte[] magic = reader.ReadBytes(4);
int version = reader.ReadInt32();
string name = reader.ReadString();
int score = reader.ReadInt32();

Console.WriteLine(Encoding.ASCII.GetString(magic));
Console.WriteLine(version);
Console.WriteLine(name);
Console.WriteLine(score);

独自フォーマットを作るときは、「何バイト目に何のデータがあるか」「どの順番で読み書きするか」を明確に決めておくことが重要です。

7. C#のバイナリ変換で注意すべきポイント

C#でバイナリを扱うときは、単に読み書きできればよいわけではありません。

エンディアン、文字コード、データサイズ、メモリ使用量などを意識しないと、読み込んだデータが壊れて見えたり、別環境で正しく読めなかったりすることがあります。

7-1. エンディアンの違いに注意する

エンディアンとは、複数バイトで表される数値をどの順番で保存するかのルールです。

例えば、intは通常4バイトです。この4バイトを小さい桁から並べる方式をリトルエンディアン、大きい桁から並べる方式をビッグエンディアンと呼びます。

BitConverterは実行環境のエンディアンに依存します。

C#
Console.WriteLine(BitConverter.IsLittleEndian);

多くの環境ではリトルエンディアンですが、ファイル形式や通信プロトコルによってはビッグエンディアンが指定されていることがあります。

エンディアンを明示したい場合は、System.Buffers.Binary名前空間のBinaryPrimitivesを使う方法があります。

C#
using System.Buffers.Binary;

byte[] data = new byte[4];

BinaryPrimitives.WriteInt32BigEndian(data, 123456);

int value = BinaryPrimitives.ReadInt32BigEndian(data);

C#でバイナリデータを他のシステムとやり取りする場合は、エンディアンを必ず確認しましょう。

7-2. 文字コードを指定しないと文字化けする理由

文字列をバイナリに変換するときは、文字コードを指定する必要があります。

同じ文字でも、UTF-8、UTF-16、Shift_JISなどでバイト列が異なります。

C#
string text = "あ";

byte[] utf8 = Encoding.UTF8.GetBytes(text);
byte[] unicode = Encoding.Unicode.GetBytes(text);

Console.WriteLine(Convert.ToHexString(utf8));
Console.WriteLine(Convert.ToHexString(unicode));

文字コードが違うと、同じ文字でも出力されるバイト列が変わります。

そのため、UTF-8で保存した文字列をShift_JISとして読み込むと、文字化けすることがあります。

C#
byte[] bytes = Encoding.UTF8.GetBytes("こんにちは");

string text = Encoding.UTF8.GetString(bytes);

保存時と読み込み時で同じ文字コードを使うことが重要です。

C#でバイナリファイル内に文字列を保存する場合は、UTF-8などの文字コードを明示しておくと安全です。

7-3. 読み込むバイト数とデータ型のサイズを合わせる

バイナリデータを数値に変換するときは、必要なバイト数を正しく読み込む必要があります。

例えば、intは4バイトです。

C#
byte[] bytes = new byte[4] { 0x01, 0x00, 0x00, 0x00 };

int value = BitConverter.ToInt32(bytes, 0);

Console.WriteLine(value);

もし3バイトしかないデータをintとして読み込もうとすると、正しく変換できません。

C#
byte[] bytes = new byte[3] { 0x01, 0x00, 0x00 };

// これは例外の原因になる
// int value = BitConverter.ToInt32(bytes, 0);

主なデータ型のサイズは次の通りです。

short: 2バイト
int: 4バイト
long: 8バイト
float: 4バイト
double: 8バイト

バイナリファイルを読むときは、どの位置から何バイト読むのかを正確に把握しておきましょう。

7-4. ファイルサイズが大きい場合は一括読み込みを避ける

File.ReadAllBytesは便利ですが、ファイル全体をメモリに読み込みます。

小さなファイルであれば問題ありませんが、大きなファイルではメモリ使用量が増えすぎる可能性があります。

C#
byte[] data = File.ReadAllBytes("large.bin");

大きなファイルを扱う場合は、FileStreamで分割して読み込みます。

C#
using FileStream fs = new FileStream("large.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[8192];
int bytesRead;

while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// bytesRead分だけ処理する
}

C#でバイナリファイルを扱うときは、ファイルサイズに応じて読み込み方法を選ぶことが大切です。

7-5. バイナリデータをそのまま文字列として扱わない

画像やPDFなどのバイナリデータを、そのまま文字列として扱うのは避けましょう。

C#
byte[] data = File.ReadAllBytes("image.png");

// 画像データを無理に文字列化するのは危険
string text = Encoding.UTF8.GetString(data);

このようにすると、文字列として解釈できないバイトが含まれていた場合、データが壊れる可能性があります。

バイナリデータを文字列として保存・送信したい場合は、Base64を使います。

C#
byte[] data = File.ReadAllBytes("image.png");

string base64 = Convert.ToBase64String(data);

byte[] restored = Convert.FromBase64String(base64);

Base64なら、バイナリデータを安全に文字列として表現できます。

8. よくあるエラーと対処法

C#でバイナリファイルを扱っていると、ファイルが開けない、読み込み途中で失敗する、データが壊れて見えるなどの問題が発生することがあります。

ここでは、よくあるエラーと対処法を解説します。

8-1. IOExceptionが発生する原因と対処

IOExceptionは、ファイル入出力に関する一般的なエラーです。

例えば、ファイルが他のプロセスで使用中の場合、ディスクに問題がある場合、読み書き中にエラーが発生した場合などに起こります。

C#
try
{
byte[] data = File.ReadAllBytes("sample.bin");
}
catch (IOException ex)
{
Console.WriteLine($"入出力エラー: {ex.Message}");
}

対処法としては、ファイルが存在するか、他のアプリで開かれていないか、読み書き可能な状態かを確認します。

また、usingを使ってファイルを確実に閉じることも重要です。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

ファイルを閉じ忘れると、自分のアプリ内でロックしたままになり、次の処理でIOExceptionが発生することがあります。

8-2. UnauthorizedAccessExceptionが発生する原因と対処

UnauthorizedAccessExceptionは、アクセス権限がない場合に発生します。

よくある原因は、書き込み権限がないフォルダに保存しようとしている、読み取り専用ファイルに書き込もうとしている、フォルダをファイルとして開こうとしている、などです。

C#
try
{
File.WriteAllBytes(@"C:\sample.bin", new byte[] { 1, 2, 3 });
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
}

対処法としては、ユーザーが書き込み可能なフォルダを使います。

C#
string folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string path = Path.Combine(folder, "sample.bin");

File.WriteAllBytes(path, new byte[] { 1, 2, 3 });

アプリケーションの設定データなどは、LocalApplicationDataApplicationDataなどのフォルダに保存するとよいでしょう。

8-3. EndOfStreamExceptionが発生する原因と対処

EndOfStreamExceptionは、BinaryReaderで読み込み中にストリームの末尾に達した場合に発生します。

例えば、4バイト必要なintを読み込もうとしたのに、残りが2バイトしかない場合などです。

C#
try
{
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

int value = reader.ReadInt32();
}
catch (EndOfStreamException)
{
Console.WriteLine("ファイルの末尾に達しました。データが不足しています。");
}

対処法としては、読み込む前に残りの長さを確認します。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

if (fs.Length - fs.Position >= 4)
{
int value = reader.ReadInt32();
Console.WriteLine(value);
}
else
{
Console.WriteLine("intを読み込むためのバイト数が足りません。");
}

独自フォーマットのファイルを読む場合は、ファイルサイズやヘッダー情報を確認してから読み込むと安全です。

8-4. 読み込んだデータが壊れて見える場合の確認ポイント

バイナリファイルを読み込んだときにデータが壊れて見える場合は、いくつかのポイントを確認しましょう。

まず、読み込み位置が正しいか確認します。Seekの位置がずれていると、意図しないバイト列を数値や文字列として解釈してしまいます。

次に、読み込み順序が書き込み順序と一致しているか確認します。

C#
// 書き込み
writer.Write(100);
writer.Write("Taro");

// 読み込みも同じ順番にする
int id = reader.ReadInt32();
string name = reader.ReadString();

書き込み時と読み込み時で順番が違うと、正しく復元できません。

また、文字コードが一致しているか、エンディアンが一致しているかも重要です。

外部システムが作成したバイナリファイルを読む場合は、そのファイル形式の仕様を確認しましょう。

8-5. ファイルがロックされて開けない場合の対処

ファイルがロックされていると、C#から開けない場合があります。

原因としては、他のアプリがファイルを使用中、自分のプログラムでFileStreamを閉じ忘れている、別スレッドが同じファイルを開いている、などが考えられます。

まずは、usingを使ってファイルを確実に閉じましょう。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);

// 読み込み処理

共有モードを指定したい場合は、FileShareを使います。

C#
using FileStream fs = new FileStream(
"sample.bin",
FileMode.Open,
FileAccess.Read,
FileShare.Read
);

FileShare.Readを指定すると、他のプロセスも読み取り用に開ける可能性があります。

ただし、書き込み中のファイルを無理に読むと不完全なデータを読み込む可能性があるため、ファイルの使用タイミングには注意が必要です。

9. C#でバイナリを扱うときのベストプラクティス

C#でバイナリを安全に扱うには、状況に応じたクラスの使い分けと、文字コード・エンディアン・リソース解放への注意が重要です。

ここでは、実務でも意識したいベストプラクティスを紹介します。

9-1. 小さいファイルはFile.ReadAllBytesを使う

小さいバイナリファイルを扱う場合は、File.ReadAllBytesが簡単で分かりやすいです。

C#
byte[] data = File.ReadAllBytes("sample.bin");

書き込みもFile.WriteAllBytesで簡単にできます。

C#
File.WriteAllBytes("sample.bin", data);

設定ファイル、小さな画像、テスト用のバイナリデータなどであれば、この方法で十分なことが多いです。

ただし、ファイルサイズが大きい場合はメモリ使用量が増えるため、FileStreamを検討しましょう。

9-2. 大きいファイルはFileStreamで分割処理する

大きなファイルを扱う場合は、FileStreamで分割処理するのが基本です。

C#
using FileStream fs = new FileStream("large.bin", FileMode.Open, FileAccess.Read);

byte[] buffer = new byte[8192];
int bytesRead;

while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// 読み込んだ分だけ処理
}

この方法なら、ファイル全体をメモリに読み込まずに済みます。

ログファイル、動画ファイル、大きな画像ファイル、バックアップファイルなどを扱う場合に有効です。

9-3. 型付きデータはBinaryReader・BinaryWriterを使う

intfloatdoublestringなどの型付きデータをバイナリファイルに保存する場合は、BinaryReaderBinaryWriterを使うと便利です。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write);
using BinaryWriter writer = new BinaryWriter(fs);

writer.Write(1);
writer.Write(3.14);
writer.Write("Hello");

読み込み側では同じ順番で読みます。

C#
using FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

int id = reader.ReadInt32();
double value = reader.ReadDouble();
string text = reader.ReadString();

独自フォーマットを作る場合は、保存する順番と型を仕様として明確にしておきましょう。

9-4. usingまたはusing宣言でリソースを確実に解放する

ファイル操作では、リソースの解放が非常に重要です。

FileStreamBinaryReaderBinaryWriterなどは、使い終わったら閉じる必要があります。

C#ではusingを使うことで、自動的に解放できます。

C#
using FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read);
using BinaryReader reader = new BinaryReader(fs);

int value = reader.ReadInt32();

usingブロックを使う書き方でも問題ありません。

C#
using (FileStream fs = new FileStream("sample.bin", FileMode.Open, FileAccess.Read))
using (BinaryReader reader = new BinaryReader(fs))
{
int value = reader.ReadInt32();
}

ファイルを閉じ忘れると、ファイルロックや書き込み失敗の原因になります。C#でバイナリファイルを扱うときは、必ずusingを使う習慣をつけましょう。

9-5. 変換処理では文字コードとエンディアンを明示する

バイナリ変換では、文字コードとエンディアンを明示することが重要です。

文字列を扱う場合は、Encoding.UTF8などを明示します。

C#
byte[] bytes = Encoding.UTF8.GetBytes("こんにちは");
string text = Encoding.UTF8.GetString(bytes);

数値を外部形式として保存する場合は、必要に応じてエンディアンを明示します。

C#
using System.Buffers.Binary;

byte[] data = new byte[4];

BinaryPrimitives.WriteInt32LittleEndian(data, 1234);

int value = BinaryPrimitives.ReadInt32LittleEndian(data);

自分のアプリ内だけで完結する場合は問題が出にくいですが、別のアプリ、別のOS、別の言語とデータをやり取りする場合は、文字コードとエンディアンの違いが不具合の原因になります。

C#でバイナリを扱うときは、「どの形式で保存し、どの形式で読み込むのか」を明確にしておきましょう。

まとめ

C#でバイナリを扱う基本は、byte[]を理解することです。

小さなバイナリファイルを読み書きするなら、File.ReadAllBytesFile.WriteAllBytesが簡単です。ファイル全体をbyte[]として扱えるため、初心者にも分かりやすい方法です。

一方、大きなファイルや一部だけを読み書きしたい場合は、FileStreamを使います。ReadWriteSeekを使うことで、バイト単位の細かい操作ができます。

数値や文字列などの型付きデータをバイナリファイルに保存する場合は、BinaryReaderBinaryWriterが便利です。書き込んだ順番と同じ順番で読み込むことが重要です。

また、byte[]と数値、文字列、Base64、16進数文字列の変換方法も、C#のバイナリ操作ではよく使います。特に文字列変換では文字コード、数値変換ではエンディアンに注意しましょう。

C#でバイナリを安全に扱うためのポイントは、次の通りです。

小さいファイルはFile.ReadAllBytesを使う
大きいファイルはFileStreamで分割処理する
型付きデータはBinaryReader・BinaryWriterを使う
usingでファイルを確実に閉じる
文字コードとエンディアンを明示する
バイナリデータを無理に文字列として扱わない

これらを押さえておけば、C#でバイナリファイルの読み書き、変換、ファイル操作を安心して実装できるようになります。