C# Streamの使い方を基礎から解説|ファイル読み書き・MemoryStream・usingまで初心者向け

はじめに

C#でファイルの読み書き、画像やPDFなどのバイナリ処理、ネットワーク通信、メモリ上のデータ操作を行うときによく登場するのがStreamです。

Streamは初心者にとって少し抽象的に感じやすいクラスですが、考え方はそれほど難しくありません。簡単に言えば、C# Streamは「データを少しずつ読み書きするための仕組み」です。

この記事では、C# Streamの基礎から、FileStreamMemoryStreamStreamReaderStreamWriterusingによる安全なリソース解放、非同期処理、よくあるエラーまで、初心者向けにわかりやすく解説します。

1. C#のStreamとは?基礎からわかりやすく解説

1-1. Streamはデータの読み書きを扱うための「流れ」

C#のStreamは、データを「流れ」として扱うための抽象クラスです。Microsoftの公式ドキュメントでも、Streamはバイト列に対する汎用的なビューを提供する抽象クラスとして説明されています。つまり、ファイルやメモリ、ネットワークなど、実体が何であっても「バイトの流れ」として読み書きできるようにする仕組みです。

たとえば、テキストファイルを読む場合も、画像ファイルを読む場合も、内部的にはバイトデータを扱います。Streamを使うことで、そのデータを先頭から順番に読み込んだり、必要な位置に移動して書き込んだりできます。

イメージとしては、水道管の中を水が流れるように、データがプログラムと保存先の間を流れていると考えるとわかりやすいです。

C#
Stream stream;

ただし、Stream自体は抽象クラスなので、通常は直接インスタンス化しません。実際には、FileStreamMemoryStreamなど、Streamを継承したクラスを使います。

1-2. Streamが使われる場面:ファイル・メモリ・ネットワーク

C# Streamは、さまざまな場面で使われます。代表的なものは次のとおりです。

用途主なクラス説明
ファイル操作FileStreamファイルをバイト単位で読み書きする
メモリ操作MemoryStreamメモリ上に一時的なデータを保持する
テキスト操作StreamReader / StreamWriter文字列として読み書きする
ネットワーク通信NetworkStreamTCP通信などでデータを送受信する
圧縮処理GZipStream圧縮・展開しながら読み書きする

たとえば、ファイルをコピーする処理、アップロードされた画像を一時的にメモリ上で処理する機能、APIから受け取ったレスポンスを読み取る処理などでもStreamが使われます。

1-3. Streamを理解するとできること

C# Streamを理解すると、次のような処理を自分で実装できるようになります。

ファイルを読み込む、ファイルに書き込む、画像やPDFをバイト配列として扱う、メモリ上でデータを加工する、大きなファイルを少しずつ処理する、非同期でファイルを読み書きする、といった実践的な処理です。

特に重要なのは、「大きなデータを一度に全部読み込まなくてもよい」という点です。たとえば数GBのファイルを扱う場合、全データを一括でメモリに読み込むとメモリ不足になる可能性があります。Streamを使えば、一定サイズずつ分割して処理できます。

1-4. byte配列・文字列・ファイルとの関係

Streamの基本単位はbyteです。文字列も画像もPDFも、コンピューター内部では最終的にバイトデータとして扱われます。

たとえば、文字列をStreamに書き込む場合は、まずEncoding.UTF8.GetBytesなどで文字列をbyte[]に変換します。

C#
using System.Text;

string text = "Hello Stream";
byte[] bytes = Encoding.UTF8.GetBytes(text);

逆に、Streamから読み込んだbyte[]を文字列に戻す場合は、Encoding.UTF8.GetStringを使います。

C#
string result = Encoding.UTF8.GetString(bytes);

つまり、C# Streamを理解するうえでは、次の関係を押さえておくとわかりやすいです。

文字列 ⇔ byte配列 ⇔ Stream ⇔ ファイル・メモリ・ネットワーク

テキストを扱う場合はStreamReaderStreamWriterを使うと便利ですが、基本的な仕組みとしては、Streamはバイトの読み書きを行っていると理解しておきましょう。

2. C# Streamの基本的な使い方

2-1. Streamクラスの主な役割

Streamクラスの主な役割は、データの読み込み、書き込み、現在位置の管理、バッファの反映です。公式ドキュメントでも、Streamの基本操作として読み取り、書き込み、シークが説明されています。また、Streamによっては読み取り専用、書き込み専用、位置移動不可の場合があります。

代表的なメンバーは次のとおりです。

メンバー役割
ReadStreamからデータを読み込む
WriteStreamへデータを書き込む
Position現在の読み書き位置を取得・設定する
LengthStream全体の長さを取得する
Seek指定した位置へ移動する
Flushバッファ内のデータを反映する
CanRead読み込み可能か確認する
CanWrite書き込み可能か確認する
CanSeek位置移動可能か確認する

Streamを扱うときは、まず「このStreamは読み込めるのか」「書き込めるのか」「位置を移動できるのか」を意識するとエラーを避けやすくなります。

2-2. Readメソッドでデータを読み込む

Readメソッドは、Streamから指定したバッファにデータを読み込みます。

C#
using System;
using System.IO;
using System.Text;

string path = "sample.txt";

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

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

string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(text);

Readメソッドの戻り値は、実際に読み込んだバイト数です。必ずしも指定したバッファサイズ分だけ読み込まれるとは限りません。

C#
int bytesRead = stream.Read(buffer, 0, buffer.Length);

戻り値が0の場合は、Streamの末尾に到達したことを意味します。そのため、ファイル全体を読み込む場合は、次のようにループで処理します。

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

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

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
Console.WriteLine($"読み込んだバイト数: {bytesRead}");
}

2-3. Writeメソッドでデータを書き込む

Writeメソッドは、byte[]の内容をStreamへ書き込みます。

C#
using System.IO;
using System.Text;

string path = "output.txt";
string text = "C# Streamの書き込みサンプルです。";

byte[] bytes = Encoding.UTF8.GetBytes(text);

using FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write);
stream.Write(bytes, 0, bytes.Length);

FileMode.Createを指定すると、ファイルが存在しない場合は新規作成され、存在する場合は上書きされます。

既存ファイルの末尾に追記したい場合は、FileMode.Appendを使います。

C#
using FileStream stream = new FileStream("log.txt", FileMode.Append, FileAccess.Write);

byte[] bytes = Encoding.UTF8.GetBytes("ログを追記します。\n");
stream.Write(bytes, 0, bytes.Length);

2-4. Position・Length・Seekの使い方

Positionは、現在の読み書き位置を表します。Streamでは、データを読み書きすると、その分だけPositionが進みます。

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

Console.WriteLine(stream.Position); // 0

byte[] buffer = new byte[10];
stream.Read(buffer, 0, buffer.Length);

Console.WriteLine(stream.Position); // 10

Lengthは、Stream全体の長さをバイト単位で表します。

C#
Console.WriteLine(stream.Length);

Seekを使うと、読み書き位置を移動できます。

C#
stream.Seek(0, SeekOrigin.Begin); // 先頭へ移動
stream.Seek(10, SeekOrigin.Begin); // 先頭から10バイト目へ移動
stream.Seek(-5, SeekOrigin.End); // 末尾から5バイト前へ移動

ただし、すべてのStreamでSeekが使えるわけではありません。たとえばネットワーク通信のようなStreamでは、現在位置という概念がないため、CanSeekfalseになることがあります。

C#
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}

2-5. Flushで書き込み内容を反映する

Flushは、バッファに残っているデータを実際の保存先へ反映するためのメソッドです。Streamの種類によっては、書き込みのたびに即座にファイルなどへ反映せず、一時的にバッファへ保持することがあります。Flushはそのバッファをクリアし、書き込み済みデータを基盤となるデータ先へ反映します。

C#
using FileStream stream = new FileStream("output.txt", FileMode.Create, FileAccess.Write);

byte[] bytes = Encoding.UTF8.GetBytes("Flushのサンプル");
stream.Write(bytes, 0, bytes.Length);

stream.Flush();

通常はusingでStreamを破棄すれば内部的に必要な処理が行われるため、毎回Flushを書く必要はありません。ただし、Streamを閉じる前に確実に書き込み内容を反映したい場合はFlushを使います。

3. FileStreamでファイルを読み書きする方法

3-1. FileStreamとは

FileStreamは、ファイルに対して読み書きを行うためのStreamです。Microsoftの公式ドキュメントでは、FileStreamはファイル用のStreamを提供し、同期および非同期の読み書きをサポートすると説明されています。

テキストファイルだけでなく、画像、PDF、ZIP、動画など、バイナリファイルも扱えます。

C#
using FileStream stream = new FileStream(
"sample.dat",
FileMode.OpenOrCreate,
FileAccess.ReadWrite
);

FileStreamは低レベルなファイル操作に向いています。文字列を簡単に読み書きしたいだけなら、後述するStreamReaderStreamWriterを使うほうが便利です。

3-2. FileStreamでファイルを読み込むサンプル

次のコードは、FileStreamでファイルを読み込み、UTF-8の文字列として表示するサンプルです。

C#
using System;
using System.IO;
using System.Text;

string path = "sample.txt";

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

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

string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(text);

小さなファイルであればこの書き方でも問題ありません。ただし、大きなファイルではstream.Length分の配列を一度に確保することになるため、分割して読み込むほうが安全です。

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

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

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.Write(text);
}

3-3. FileStreamでファイルを書き込むサンプル

次のコードは、文字列をUTF-8のバイト配列に変換し、FileStreamでファイルへ書き込むサンプルです。

C#
using System.IO;
using System.Text;

string path = "output.txt";
string text = "FileStreamで書き込みます。";

byte[] bytes = Encoding.UTF8.GetBytes(text);

using FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write);
stream.Write(bytes, 0, bytes.Length);

FileMode.Createを使うと上書きされます。既存ファイルを残したまま末尾に追加したい場合は、次のようにします。

C#
using FileStream stream = new FileStream("output.txt", FileMode.Append, FileAccess.Write);

byte[] bytes = Encoding.UTF8.GetBytes("追記するテキスト\n");
stream.Write(bytes, 0, bytes.Length);

3-4. FileMode・FileAccess・FileShareの違い

FileStreamのコンストラクターでよく使うのが、FileModeFileAccessFileShareです。

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

それぞれの意味は次のとおりです。

種類役割
FileModeファイルをどう開くか新規作成、上書き、追記など
FileAccess読み書きの権限読み込みのみ、書き込みのみ、両方
FileShare他の処理からの共有方法他の読み込みを許可するかなど

代表的なFileModeは次のとおりです。

説明
Create新規作成。存在する場合は上書き
CreateNew新規作成。存在する場合は例外
Open既存ファイルを開く。存在しない場合は例外
OpenOrCreate存在すれば開き、なければ作成
Append末尾に追記

FileAccessには、ReadWriteReadWriteがあります。

FileShareは、ファイルロックに関係します。たとえばFileShare.Readを指定すると、他の処理による読み込みを許可できます。

3-5. ファイルが存在しない場合の対処法

FileMode.Openで存在しないファイルを開こうとすると例外が発生します。存在しない可能性がある場合は、事前にFile.Existsで確認するか、FileMode.OpenOrCreateを使います。

C#
string path = "sample.txt";

if (File.Exists(path))
{
using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read);
}
else
{
Console.WriteLine("ファイルが存在しません。");
}

ファイルがなければ作成したい場合は、次のようにします。

C#
using FileStream stream = new FileStream(
"sample.txt",
FileMode.OpenOrCreate,
FileAccess.ReadWrite
);

ただし、OpenOrCreateで新規作成されたファイルは空です。読み込む前にLengthを確認すると安全です。

C#
if (stream.Length == 0)
{
Console.WriteLine("ファイルは空です。");
}

4. StreamReaderとStreamWriterで文字列を扱う

4-1. StreamとStreamReader・StreamWriterの違い

StreamFileStreamは、基本的にバイト単位でデータを扱います。一方、StreamReaderStreamWriterは、文字列を扱うためのクラスです。

クラス主な用途
Streamバイト列を読み書きする抽象クラス
FileStreamファイルをバイト単位で読み書きする
StreamReaderStreamから文字列を読み込む
StreamWriterStreamへ文字列を書き込む

テキストファイルを扱うだけなら、FileStreambyte[]を変換するより、StreamReaderStreamWriterを使うほうが簡単です。

4-2. StreamReaderでテキストファイルを読み込む

StreamReaderを使うと、テキストファイルを簡単に読み込めます。

C#
using System;
using System.IO;
using System.Text;

string path = "sample.txt";

using StreamReader reader = new StreamReader(path, Encoding.UTF8);

string text = reader.ReadToEnd();
Console.WriteLine(text);

1行ずつ読み込みたい場合は、ReadLineを使います。

C#
using StreamReader reader = new StreamReader("sample.txt", Encoding.UTF8);

string? line;

while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}

大きなテキストファイルを扱う場合は、ReadToEndで一括読み込みするより、ReadLineで1行ずつ処理するほうがメモリ効率がよくなります。

4-3. StreamWriterでテキストファイルを書き込む

StreamWriterを使うと、文字列をそのままファイルへ書き込めます。

C#
using System.IO;
using System.Text;

string path = "output.txt";

using StreamWriter writer = new StreamWriter(path, false, Encoding.UTF8);

writer.WriteLine("1行目");
writer.WriteLine("2行目");
writer.WriteLine("C# StreamWriterのサンプルです。");

第2引数にfalseを指定すると上書き、trueを指定すると追記になります。

C#
using StreamWriter writer = new StreamWriter("output.txt", true, Encoding.UTF8);

writer.WriteLine("追記する行です。");

StreamWriterは内部でバッファリングするため、必要に応じてFlushを呼び出すこともできます。

C#
writer.Flush();

ただし、usingを使っていれば、スコープを抜けるときに自動的に破棄され、必要な書き込み処理も行われます。

4-4. Encodingで文字コードを指定する方法

文字列を扱うときに重要なのが文字コードです。C#ではEncodingを指定して、UTF-8やShift_JISなどを選べます。

UTF-8で読み込む例です。

C#
using StreamReader reader = new StreamReader("sample.txt", Encoding.UTF8);

UTF-8で書き込む例です。

C#
using StreamWriter writer = new StreamWriter("output.txt", false, Encoding.UTF8);

Shift_JISを使いたい場合、.NET Coreや.NET 5以降ではコードページの登録が必要になる場合があります。

C#
using System.Text;

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

Encoding shiftJis = Encoding.GetEncoding("shift_jis");

using StreamReader reader = new StreamReader("sample.txt", shiftJis);

通常、新規に作成するファイルではUTF-8を使うのがおすすめです。

4-5. 文字化けを防ぐポイント

文字化けの多くは、「書き込んだ文字コード」と「読み込むときの文字コード」が一致していないことが原因です。

たとえば、Shift_JISで保存されたファイルをUTF-8として読み込むと、文字化けすることがあります。

C#
using StreamReader reader = new StreamReader("sjis.txt", Encoding.UTF8);

このような場合は、実際の文字コードに合わせて読み込む必要があります。

C#
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

using StreamReader reader = new StreamReader(
"sjis.txt",
Encoding.GetEncoding("shift_jis")
);

文字化けを防ぐためのポイントは、入力ファイルの文字コードを確認する、新規作成するファイルはUTF-8で統一する、外部システムと連携する場合は仕様書の文字コードを確認する、の3つです。

5. MemoryStreamの使い方

5-1. MemoryStreamとは

MemoryStreamは、メモリ上にデータを保持するためのStreamです。ファイルに保存せず、一時的にバイトデータを扱いたい場合に使います。MemoryStreamStreamを継承したクラスであり、バイト列をメモリ上で読み書きできます。

たとえば、次のような場面で使われます。

画像データをメモリ上で加工する、APIから受け取ったバイナリデータを一時保存する、ファイルに保存する前に内容を組み立てる、テスト用にStreamを用意する、といったケースです。

5-2. MemoryStreamでbyte配列を扱う方法

既存のbyte[]からMemoryStreamを作成できます。

C#
using System.IO;
using System.Text;

byte[] bytes = Encoding.UTF8.GetBytes("MemoryStreamのサンプル");

using MemoryStream stream = new MemoryStream(bytes);

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

string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(text);

MemoryStreamを使うと、ファイルを作らずにStreamの読み書きを試せるため、学習やテストにも便利です。

5-3. MemoryStreamに書き込んだ内容を取得する

MemoryStreamに書き込んだ内容をbyte[]として取得するには、ToArrayを使います。

C#
using System;
using System.IO;
using System.Text;

using MemoryStream stream = new MemoryStream();

byte[] bytes = Encoding.UTF8.GetBytes("メモリに書き込みます。");
stream.Write(bytes, 0, bytes.Length);

byte[] result = stream.ToArray();

Console.WriteLine(Encoding.UTF8.GetString(result));

注意点として、書き込んだ直後のPositionは末尾にあります。そのままReadしようとしても読み込めないことがあります。

C#
stream.Position = 0;

読み込み直す場合は、Positionを先頭に戻しましょう。

C#
using MemoryStream stream = new MemoryStream();

byte[] bytes = Encoding.UTF8.GetBytes("Hello");
stream.Write(bytes, 0, bytes.Length);

stream.Position = 0;

byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);

Console.WriteLine(Encoding.UTF8.GetString(buffer));

5-4. 文字列をMemoryStreamに変換する方法

文字列をMemoryStreamに変換するには、まず文字列をbyte[]に変換します。

C#
using System.IO;
using System.Text;

string text = "文字列をMemoryStreamに変換します。";

byte[] bytes = Encoding.UTF8.GetBytes(text);

using MemoryStream stream = new MemoryStream(bytes);

逆に、MemoryStreamから文字列を取得するには、ToArrayEncoding.GetStringを使います。

C#
string result = Encoding.UTF8.GetString(stream.ToArray());

StreamReaderを使って読み込むこともできます。

C#
stream.Position = 0;

using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
string result = reader.ReadToEnd();

Console.WriteLine(result);

ただし、この書き方ではStreamReaderを破棄すると、元のMemoryStreamも閉じられることがあります。後でStreamを再利用したい場合は、leaveOpen: trueを指定します。

C#
using StreamReader reader = new StreamReader(
stream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true
);

5-5. FileStreamとMemoryStreamの使い分け

FileStreamMemoryStreamは、どちらもStreamを継承していますが、用途が異なります。

クラス保存先向いている用途
FileStreamファイルファイルの読み書き
MemoryStreamメモリ一時的なデータ処理、テスト、変換処理

ファイルとして保存したい場合はFileStream、一時的にメモリ上で処理したい場合はMemoryStreamを使います。

ただし、MemoryStreamはメモリを使うため、大きすぎるデータを扱うとメモリ不足の原因になります。大容量ファイルはFileStreamで少しずつ読み書きするほうが安全です。

6. usingでStreamを安全に閉じる方法

6-1. StreamはなぜClose・Disposeが必要なのか

Streamは、ファイルハンドル、ネットワーク接続、内部バッファなどのリソースを使用します。そのため、使い終わったら必ず解放する必要があります。公式ドキュメントでも、StreamIDisposableを実装しており、使用後はDisposeするか、C#のusingなどで間接的に破棄することが推奨されています。

解放しないままにすると、ファイルがロックされたままになる、書き込み内容が反映されない、リソース不足になる、といった問題が起こる可能性があります。

6-2. using文の基本

using文を使うと、処理が終わったタイミングで自動的にDisposeが呼び出されます。C#のusing文は、IDisposableを実装したオブジェクトを正しく破棄するための構文です。

C#
using (FileStream stream = new FileStream("sample.txt", FileMode.Open))
{
// streamを使った処理
}

このブロックを抜けると、自動的にstream.Dispose()が呼び出されます。例外が発生した場合でも解放されるため、Streamを扱うときは基本的にusingを使いましょう。

6-3. using宣言の書き方

C# 8.0以降では、using宣言も使えます。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

// streamを使った処理

この場合、現在のスコープを抜けるときに自動的にDisposeされます。ネストが深くならないため、コードをすっきり書けます。

複数のStreamを扱う場合も便利です。

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

source.CopyTo(destination);

6-4. Disposeし忘れで起こる問題

StreamをDisposeし忘れると、次のような問題が起こることがあります。

ファイルが使用中のままになり、削除や上書きができない、書き込み内容が最後まで保存されない、アプリケーションが不要なリソースを保持し続ける、同じファイルを別の処理から開けない、などです。

悪い例です。

C#
FileStream stream = new FileStream("sample.txt", FileMode.Open);
// Disposeしていない

良い例です。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

特別な理由がない限り、Streamは必ずusingとセットで使うと考えておきましょう。

6-5. StreamReader・StreamWriter利用時の注意点

StreamReaderStreamWriterを破棄すると、内部で使っているStreamも一緒に閉じられることがあります。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);
using StreamReader reader = new StreamReader(stream);

string text = reader.ReadToEnd();

このコードでは、readerが破棄されると、内部のstreamも閉じられます。通常はこれで問題ありません。

ただし、StreamReaderを閉じたあとも元のStreamを使いたい場合は、leaveOpen: trueを指定します。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

using (StreamReader reader = new StreamReader(
stream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true
))
{
string text = reader.ReadToEnd();
}

stream.Position = 0;
// streamを再利用できる

7. C# Streamの実践サンプル

7-1. ファイルをコピーするサンプル

Streamを使ったファイルコピーは、CopyToを使うと簡単です。

C#
using System.IO;

string sourcePath = "source.pdf";
string destinationPath = "copy.pdf";

using FileStream source = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
using FileStream destination = new FileStream(destinationPath, FileMode.Create, FileAccess.Write);

source.CopyTo(destination);

CopyToは、現在のStreamから別のStreamへデータをコピーします。ファイルだけでなく、MemoryStreamなど別のStreamにも使えます。

7-2. 画像やPDFなどバイナリファイルを読み書きする

画像やPDFは文字列ではなくバイナリデータとして扱います。そのため、StreamReaderではなくFileStreamを使うのが基本です。

C#
using System.IO;

byte[] bytes;

using (FileStream stream = new FileStream("image.png", FileMode.Open, FileAccess.Read))
{
bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
}

using (FileStream stream = new FileStream("copy.png", FileMode.Create, FileAccess.Write))
{
stream.Write(bytes, 0, bytes.Length);
}

小さな画像であればこの書き方でも問題ありません。大きなファイルを扱う場合は、CopyToや分割読み込みを使いましょう。

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

source.CopyTo(destination);

7-3. Streamをbyte配列に変換する

Streamの内容をbyte[]に変換するには、MemoryStreamへコピーしてToArrayを使う方法が便利です。

C#
using System.IO;

static byte[] StreamToByteArray(Stream input)
{
using MemoryStream memoryStream = new MemoryStream();
input.CopyTo(memoryStream);
return memoryStream.ToArray();
}

ただし、変換前のStreamのPositionが末尾にあると、コピーされるデータが空になることがあります。先頭から読みたい場合は、可能であればPositionを戻します。

C#
if (input.CanSeek)
{
input.Position = 0;
}

7-4. byte配列をStreamに変換する

byte[]をStreamとして扱いたい場合は、MemoryStreamを使います。

C#
using System.IO;
using System.Text;

byte[] bytes = Encoding.UTF8.GetBytes("byte配列からStreamへ変換");

using MemoryStream stream = new MemoryStream(bytes);

この方法は、ファイルを使わずにStreamを受け取るメソッドをテストしたい場合にも便利です。

C#
static void PrintStream(Stream stream)
{
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());
}

byte[] bytes = Encoding.UTF8.GetBytes("テストデータ");
using MemoryStream stream = new MemoryStream(bytes);

PrintStream(stream);

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

大きなファイルを扱う場合は、一括で読み込まず、バッファを使って少しずつ処理します。

C#
using System;
using System.IO;

string path = "large.dat";

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

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

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// buffer[0] から buffer[bytesRead - 1] までを処理する
Console.WriteLine($"{bytesRead} バイト読み込みました");
}

ポイントは、buffer.Lengthではなく、必ずbytesReadの範囲だけを処理することです。最後の読み込みでは、バッファサイズより少ないバイト数になることがあります。

8. 非同期処理でStreamを扱う方法

8-1. ReadAsync・WriteAsyncとは

ReadAsyncWriteAsyncは、Streamを非同期で読み書きするためのメソッドです。StreamにはReadAsyncWriteAsyncCopyToAsyncFlushAsyncなどの非同期メソッドがあり、時間のかかるI/O処理でメインスレッドのブロックを避けるために使えます。

特に、GUIアプリ、Webアプリ、サーバー処理では、ファイルやネットワークのI/O待ちでスレッドを止めないことが重要です。

8-2. 非同期でファイルを読み込むサンプル

次のコードは、ファイルを非同期で読み込むサンプルです。

C#
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;

static async Task ReadFileAsync()
{
string path = "sample.txt";

using FileStream stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
useAsync: true
);

byte[] buffer = new byte[stream.Length];
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);

string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(text);
}

大きなファイルでは、非同期でも分割読み込みが基本です。

C#
static async Task ReadLargeFileAsync()
{
using FileStream stream = new FileStream(
"large.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 8192,
useAsync: true
);

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

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

8-3. 非同期でファイルを書き込むサンプル

WriteAsyncを使うと、ファイルへの書き込みを非同期で行えます。

C#
using System.IO;
using System.Text;
using System.Threading.Tasks;

static async Task WriteFileAsync()
{
string path = "output.txt";
string text = "非同期でファイルに書き込みます。";

byte[] bytes = Encoding.UTF8.GetBytes(text);

using FileStream stream = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true
);

await stream.WriteAsync(bytes, 0, bytes.Length);
}

StreamWriterにも非同期メソッドがあります。

C#
static async Task WriteTextAsync()
{
using StreamWriter writer = new StreamWriter("output.txt", false, Encoding.UTF8);

await writer.WriteLineAsync("StreamWriterで非同期書き込み");
}

8-4. await usingを使う場面

await usingは、非同期の破棄処理を行うための構文です。IAsyncDisposableを実装したオブジェクトに対して使えます。Streamには非同期破棄のためのDisposeAsyncも用意されています。

C#
await using FileStream stream = new FileStream(
"output.txt",
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true
);

byte[] bytes = Encoding.UTF8.GetBytes("await usingのサンプル");
await stream.WriteAsync(bytes, 0, bytes.Length);

通常のファイル操作ではusingでも十分な場合が多いですが、非同期処理で統一したい場合や、非同期破棄が重要なリソースを扱う場合はawait usingを検討します。

8-5. 同期処理と非同期処理の使い分け

同期処理と非同期処理は、次のように使い分けるとよいでしょう。

処理向いているケース
同期処理小さなファイル、コンソールツール、単純な処理
非同期処理Webアプリ、GUIアプリ、大きなファイル、ネットワークI/O

初心者のうちは、まず同期処理でStreamの基本を理解し、その後でReadAsyncWriteAsyncを使うとスムーズです。

Webアプリでは、I/O待ちの間にスレッドを有効活用できるため、非同期処理がよく使われます。

9. C# Streamでよくあるエラーと対処法

9-1. ObjectDisposedExceptionが発生する原因

ObjectDisposedExceptionは、すでにDisposeされたStreamを使おうとしたときに発生します。

C#
FileStream stream;

using (stream = new FileStream("sample.txt", FileMode.Open))
{
}

// ここではすでにDispose済み
stream.ReadByte(); // ObjectDisposedException

対処法は、usingのスコープ内でStreamを使い切ることです。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

int value = stream.ReadByte();

StreamReaderStreamWriterを使う場合も、内部のStreamが閉じられていないか注意しましょう。

9-2. UnauthorizedAccessExceptionが発生する原因

UnauthorizedAccessExceptionは、アクセス権限がないファイルやディレクトリにアクセスしたときに発生します。

よくある原因は、読み取り専用ファイルに書き込もうとしている、管理者権限が必要な場所に書き込もうとしている、ディレクトリをファイルとして開こうとしている、別プロセスの制限によりアクセスできない、などです。

C#
using FileStream stream = new FileStream(
@"C:\Windows\system32\sample.txt",
FileMode.Create,
FileAccess.Write
);

対処法としては、書き込み可能なフォルダを使う、ファイルの権限を確認する、ファイルパスがディレクトリではないか確認する、などがあります。

ユーザーごとのデータ保存には、次のような場所を使うと安全です。

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

9-3. IOExceptionが発生する原因

IOExceptionは、入出力処理で問題が起きたときに発生します。

代表的な原因は、ファイルが他のプロセスに使用されている、ディスク容量が不足している、ファイルパスが不正、読み書き中にデバイスが切断された、などです。

C#
try
{
using FileStream stream = new FileStream("sample.txt", FileMode.Open, FileAccess.ReadWrite);
}
catch (IOException ex)
{
Console.WriteLine($"I/Oエラー: {ex.Message}");
}

ファイルロックが原因の場合は、FileShareを適切に指定することで改善できる場合があります。

C#
using FileStream stream = new FileStream(
"sample.txt",
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite
);

9-4. Positionが末尾のままで読み込めない場合

MemoryStreamでよくあるのが、書き込み後にそのまま読み込もうとして、何も読めないケースです。

C#
using MemoryStream stream = new MemoryStream();

byte[] bytes = Encoding.UTF8.GetBytes("Hello");
stream.Write(bytes, 0, bytes.Length);

// Positionが末尾のため、このまま読むと読めない
byte[] buffer = new byte[stream.Length];
int bytesRead = stream.Read(buffer, 0, buffer.Length);

Console.WriteLine(bytesRead); // 0

対処法は、読み込み前にPositionを先頭へ戻すことです。

C#
stream.Position = 0;

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

Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));

Streamを読み書きするとPositionが移動する、という基本を覚えておきましょう。

9-5. ファイルロックを避ける方法

ファイルロックを避けるには、まずusingで確実にStreamを閉じることが重要です。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
// 処理

次に、他のプロセスとの共有を許可する場合はFileShareを指定します。

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

読み込みだけでよい場合はFileAccess.Readを使い、不要にReadWriteを指定しないことも大切です。

また、Streamを長時間開きっぱなしにしないようにしましょう。必要なタイミングで開き、処理が終わったらすぐ閉じるのが基本です。

10. C# Streamを使うときの注意点とベストプラクティス

10-1. 大きなデータは一括読み込みしない

大きなファイルを扱うときに、次のようなコードを書くとメモリを大量に消費します。

C#
byte[] bytes = File.ReadAllBytes("large.dat");

小さなファイルなら問題ありませんが、大容量ファイルでは危険です。Streamを使って分割処理しましょう。

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

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

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// 必要な分だけ処理
}

10-2. usingで確実にリソースを解放する

Streamを使うときは、基本的にusingを使います。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

Closeを手動で呼び出すより、usingで自動的にDisposeされるようにするほうが安全です。例外が発生した場合でも解放されます。

10-3. 文字列処理ではStreamReader・StreamWriterを使う

テキストファイルを扱う場合は、FileStreamでバイト配列を直接処理するより、StreamReaderStreamWriterを使うほうが簡単です。

C#
using StreamReader reader = new StreamReader("sample.txt", Encoding.UTF8);
string text = reader.ReadToEnd();

書き込みも同様です。

C#
using StreamWriter writer = new StreamWriter("output.txt", false, Encoding.UTF8);
writer.WriteLine("テキストを書き込みます。");

文字コードも明示できるため、文字化け対策にもなります。

10-4. バイナリ処理ではFileStream・MemoryStreamを使う

画像、PDF、ZIP、動画などのバイナリファイルは、文字列として扱わず、FileStreamMemoryStreamで処理します。

C#
using FileStream stream = new FileStream("image.png", FileMode.Open, FileAccess.Read);

一時的にメモリ上で扱いたい場合はMemoryStreamを使います。

C#
using MemoryStream memoryStream = new MemoryStream();

テキストなのかバイナリなのかによって、使うクラスを分けることが大切です。

10-5. 例外処理を適切に入れる

ファイル操作では、ファイルが存在しない、権限がない、他のプロセスが使用中、ディスク容量不足など、さまざまな例外が起こり得ます。

C#
try
{
using FileStream stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);

// 読み込み処理
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"入出力エラーが発生しました: {ex.Message}");
}

すべての例外をむやみに握りつぶすのではなく、原因に応じて適切に対処しましょう。

11. C# Streamに関するよくある質問

11-1. StreamとFileStreamの違いは?

Streamは、データの読み書きを抽象化した基底クラスです。FileStreamは、そのStreamを継承したファイル操作用のクラスです。

Stream
└── FileStream
└── MemoryStream
└── NetworkStream

つまり、Streamは共通の仕組み、FileStreamはファイルを扱う具体的な実装です。

11-2. StreamReaderとFileStreamはどちらを使うべき?

テキストファイルを文字列として読みたいならStreamReaderを使います。

C#
using StreamReader reader = new StreamReader("sample.txt", Encoding.UTF8);

画像やPDFなどのバイナリファイルを扱うならFileStreamを使います。

C#
using FileStream stream = new FileStream("image.png", FileMode.Open, FileAccess.Read);

簡単に言えば、文字列ならStreamReader、バイナリならFileStreamです。

11-3. MemoryStreamはいつ使う?

MemoryStreamは、ファイルに保存せず、メモリ上で一時的にデータを扱いたいときに使います。

たとえば、APIレスポンスのバイナリデータを一時的に処理する、画像をメモリ上で加工する、テスト用のStreamを作る、文字列やbyte配列をStreamとして扱う、といった場面です。

C#
byte[] bytes = Encoding.UTF8.GetBytes("test");
using MemoryStream stream = new MemoryStream(bytes);

11-4. Streamをstringに変換するには?

テキストデータが入っているStreamであれば、StreamReaderを使って文字列に変換できます。

C#
using System.IO;
using System.Text;

static string StreamToString(Stream stream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}

using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
return reader.ReadToEnd();
}

元のStreamを閉じたくない場合は、leaveOpen: trueを指定します。

C#
static string StreamToStringLeaveOpen(Stream stream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}

using StreamReader reader = new StreamReader(
stream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true
);

return reader.ReadToEnd();
}

11-5. Streamをbyte配列に変換するには?

Streamをbyte[]に変換するには、MemoryStreamへコピーします。

C#
static byte[] StreamToBytes(Stream stream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}

using MemoryStream memoryStream = new MemoryStream();
stream.CopyTo(memoryStream);
return memoryStream.ToArray();
}

大きなStreamをbyte[]に変換するとメモリを大量に使うため、必要な場合だけ使いましょう。

11-6. CloseとDisposeの違いは?

CloseはStreamを閉じるためのメソッドで、Disposeは使用しているリソースを解放するためのメソッドです。現在のC#では、Streamを扱うときはCloseを直接呼ぶより、usingDisposeを自動的に呼び出す書き方が一般的です。公式ドキュメントでも、Closeよりも適切に破棄することが案内されています。

C#
using FileStream stream = new FileStream("sample.txt", FileMode.Open);

この書き方なら、スコープを抜けるときに自動的にリソースが解放されます。

まとめ

C# Streamは、ファイル、メモリ、ネットワークなどのデータを「バイトの流れ」として扱うための重要な仕組みです。

Streamは抽象クラスであり、実際にはFileStreamMemoryStreamNetworkStreamなどの派生クラスを使います。ファイルをバイナリとして扱うならFileStream、メモリ上で一時的に扱うならMemoryStream、テキストを扱うならStreamReaderStreamWriterを使うのが基本です。

また、Streamはファイルハンドルやバッファなどのリソースを使うため、usingで確実にDisposeすることが重要です。大きなファイルは一括読み込みせず、バッファを使って分割処理しましょう。

C# Streamを理解すると、ファイル読み書き、画像やPDFの処理、非同期I/O、メモリ上でのデータ変換など、実務でよく使う処理を柔軟に実装できるようになります。まずはFileStreamStreamReaderStreamWriterMemoryStreamの基本的な使い方から覚えていきましょう。