C# Streamの使い方を基礎から解説!ファイル読み書き・MemoryStream・非同期処理までわかる入門ガイド

はじめに

C#でファイルの読み書き、画像やPDFなどのバイナリ処理、Web APIのレスポンス処理、メモリ上での一時データ操作を行うときに欠かせないのがStreamです。

Streamは、データを「読み込む」「書き込む」「移動する」といった操作を共通の形で扱うための仕組みです。ファイルを扱うFileStream、メモリ上のデータを扱うMemoryStream、ネットワーク通信で使うNetworkStreamなど、C#にはさまざまなStreamがあります。

この記事では、C# Streamの基本概念から、FileStreamによるファイル読み書き、StreamReader・StreamWriter、MemoryStream、バイナリデータ、非同期処理、よくあるエラーまで、初心者にもわかりやすく解説します。

1. C#のStreamとは?データを読み書きするための基本概念

1-1. Streamは「データの流れ」を扱う抽象クラス

C#のStreamは、データの読み書きを行うための抽象クラスです。

抽象クラスとは、それ自体を直接使うというより、共通のルールを定義するためのクラスです。実際には、FileStreamMemoryStreamなど、Streamを継承した具体的なクラスを使います。

たとえば、ファイルからデータを読む場合も、メモリ上のデータを読む場合も、基本的には次のような考え方になります。

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

ここで重要なのは、読み込み元がファイルなのかメモリなのかを意識せず、Readという共通の操作で扱える点です。

1-2. ファイル・メモリ・ネットワークで共通して使える理由

Streamは「データの入出力先」を抽象化しています。

たとえば、次のようなデータのやり取りはすべてStreamで扱えます。

C#
FileStream fileStream = File.OpenRead("sample.txt");
MemoryStream memoryStream = new MemoryStream();

FileStreamはファイルを対象にしたStream、MemoryStreamはメモリ上のデータを対象にしたStreamです。

どちらもStreamを継承しているため、次のように共通の型として扱えます。

C#
void ProcessStream(Stream stream)
{
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
}

このようにしておくと、ファイルでもメモリでもネットワークでも、同じ処理に渡せる柔軟なコードになります。

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

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

ファイルを少しずつ読み込むことで、大容量ファイルでもメモリを圧迫せずに処理できます。画像、PDF、ZIPファイルなどのバイナリデータも扱えます。Web APIから受け取ったレスポンスをStreamとして処理することもできます。

また、MemoryStreamを使えば、ファイルを作成せずにメモリ上だけで一時的なデータを生成できます。CSVやJSONを一時的に作ってアップロードするような処理にも役立ちます。

1-4. Stream初心者がつまずきやすいポイント

Stream初心者がよくつまずくのは、読み書き位置を表すPositionです。

たとえば、MemoryStreamにデータを書き込んだ直後に読み込もうとしても、Positionが末尾にあるため読み込めないことがあります。

C#
using var stream = new MemoryStream();

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

// 読み込む前に位置を先頭へ戻す
stream.Position = 0;

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

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

また、Streamは使い終わったらDisposeする必要があります。特にファイルを扱う場合、Disposeを忘れるとファイルがロックされたままになることがあります。

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

2-1. Streamで扱う主な操作はRead・Write・Seek

Streamでよく使う基本操作は、主に次の3つです。

ReadはStreamからデータを読み込みます。

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

WriteはStreamへデータを書き込みます。

C#
stream.Write(buffer, 0, buffer.Length);

Seekは読み書き位置を移動します。

C#
stream.Seek(0, SeekOrigin.Begin);

ReadWriteは多くのStreamで使いますが、SeekはすべてのStreamで使えるわけではありません。たとえば、ファイルやメモリのStreamでは使えることが多いですが、ネットワーク通信のStreamでは位置を戻すことができません。

2-2. CanRead・CanWrite・CanSeekでできる操作を確認する

Streamには、現在のStreamでどの操作ができるかを確認するプロパティがあります。

C#
Console.WriteLine(stream.CanRead);
Console.WriteLine(stream.CanWrite);
Console.WriteLine(stream.CanSeek);

CanReadtrueなら読み込み可能、CanWritetrueなら書き込み可能、CanSeektrueなら位置移動可能です。

たとえば、読み取り専用で開いたFileStreamに対して書き込みを行うとエラーになります。そのため、汎用的な処理を書く場合は、事前に確認しておくと安全です。

C#
if (stream.CanRead)
{
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
}

2-3. PositionとLengthで読み書き位置とサイズを扱う

Positionは現在の読み書き位置、LengthはStream全体のサイズを表します。

C#
Console.WriteLine($"現在位置: {stream.Position}");
Console.WriteLine($"サイズ: {stream.Length}");

MemoryStreamFileStreamでは、Positionを変更して先頭に戻すことができます。

C#
stream.Position = 0;

ただし、NetworkStreamのようにCanSeekfalseのStreamでは、PositionLengthを使えない場合があります。

安全に扱うなら、次のように確認してから使います。

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

2-4. Flushでバッファの内容を確実に書き込む

Flushは、バッファに残っているデータを実際の出力先へ書き出すためのメソッドです。

C#
stream.Flush();

たとえば、ファイルに書き込む処理では、データがすぐにディスクへ書き込まれるとは限りません。効率化のために一時的にバッファへためられることがあります。

Flushを呼ぶことで、未書き込みのデータを確実に反映できます。

ただし、通常はDisposeusingによってStreamを閉じるときにもFlushされるため、毎回手動で呼ぶ必要はありません。処理の途中で確実に書き込みたい場合に使うとよいでしょう。

2-5. CloseとDisposeでリソースを解放する

Streamは、使い終わったら必ず解放する必要があります。

C#
stream.Close();

または、

C#
stream.Dispose();

現在のC#では、usingを使って自動的に解放する書き方が一般的です。

C#
using var stream = File.OpenRead("sample.txt");

// streamを使った処理

usingブロックを使う書き方もあります。

C#
using (var stream = File.OpenRead("sample.txt"))
{
// streamを使った処理
}

特にFileStreamでは、解放を忘れるとファイルが開いたままになり、別の処理から編集や削除ができないことがあります。

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

3-1. FileStreamとは?ファイル操作に使うStream

FileStreamは、ファイルを読み書きするためのStreamです。

テキストファイルだけでなく、画像、PDF、ZIP、動画など、あらゆるファイルをバイト単位で扱えます。

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

FileStreamは低レベルなファイル操作に向いています。文字列としてテキストを読みたい場合は、後述するStreamReaderStreamWriterを使うのが一般的です。

3-2. FileStreamでファイルを読み込む基本コード

FileStreamでファイルを読み込む基本コードは次のとおりです。

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

using var stream = new FileStream("sample.txt", 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);

このコードでは、ファイルをバイト配列として読み込み、UTF-8の文字列に変換しています。

ただし、大きなファイルに対してstream.Length分の配列を一度に確保すると、メモリを大量に使います。大容量ファイルでは、一定サイズのバッファで分割して読むのが基本です。

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

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

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

3-3. FileStreamでファイルに書き込む基本コード

FileStreamでファイルに書き込むには、FileAccess.Writeを指定します。

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

string text = "Hello, C# Stream!";
byte[] data = Encoding.UTF8.GetBytes(text);

using var stream = new FileStream("output.txt", FileMode.Create, FileAccess.Write);
stream.Write(data, 0, data.Length);

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

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

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

3-4. FileMode・FileAccess・FileShareの使い分け

FileStreamでは、ファイルの開き方を細かく指定できます。

FileModeは、ファイルをどのように開くかを指定します。

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

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

Openは既存ファイルを開きます。ファイルが存在しない場合は例外になります。

Createは新規作成します。すでに存在する場合は上書きします。

CreateNewは新規作成します。すでに存在する場合は例外になります。

Appendは既存ファイルの末尾に追記します。

OpenOrCreateは存在すれば開き、なければ作成します。

FileAccessは読み取り、書き込み、または両方を指定します。

C#
FileAccess.Read
FileAccess.Write
FileAccess.ReadWrite

FileShareは、他のプロセスが同じファイルにアクセスできるかを指定します。

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

この例では、自分は読み取り専用で開き、他のプロセスにも読み取りを許可しています。

3-5. using文で安全にファイルを閉じる

FileStreamはOSのファイルハンドルを使うため、必ず解放する必要があります。

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

using varを使うと、スコープを抜けたタイミングで自動的にDisposeが呼ばれます。

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

C#
using (var stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read))
{
// ファイル処理
}

どちらの書き方でも、ファイルを確実に閉じられます。

3-6. ファイルが存在しない・アクセスできない場合の例外処理

ファイル操作では、ファイルが存在しない、権限がない、別のプロセスが使用中などの理由で例外が発生することがあります。

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

byte[] buffer = new byte[stream.Length];
stream.Read(buffer, 0, buffer.Length);
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ファイルへのアクセス権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"入出力エラーが発生しました: {ex.Message}");
}

ファイル操作では例外が起こる前提で、必要に応じてtry-catchを使いましょう。

4. StreamReaderとStreamWriterでテキストファイルを扱う

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

Streamは基本的にバイト単位でデータを扱います。

一方、StreamReaderStreamWriterは、テキストを扱いやすくするためのクラスです。

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

StreamReaderはStreamから文字列を読み込むために使います。StreamWriterは文字列をStreamへ書き込むために使います。

つまり、テキストファイルを扱うならStreamReaderStreamWriterのほうが便利です。

4-2. StreamReaderでテキストを1行ずつ読み込む

テキストファイルを1行ずつ読み込むには、ReadLineを使います。

C#
using System;
using System.IO;

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

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

この方法は、ファイル全体を一度にメモリへ読み込まないため、大きめのテキストファイルにも向いています。

すべての内容を一度に読みたい場合は、ReadToEndを使います。

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

Console.WriteLine(text);

4-3. StreamWriterでテキストを書き込む

テキストファイルへ文字列を書き込むには、StreamWriterを使います。

C#
using System.IO;

using var writer = new StreamWriter("output.txt");

writer.WriteLine("1行目");
writer.WriteLine("2行目");
writer.WriteLine("3行目");

既存ファイルに追記したい場合は、コンストラクタの第2引数にtrueを指定します。

C#
using var writer = new StreamWriter("output.txt", append: true);

writer.WriteLine("追記するテキスト");

StreamWriterも内部でバッファを使うため、途中で確実に書き込みたい場合はFlushを呼び出します。

C#
writer.Flush();

4-4. 文字コードを指定して文字化けを防ぐ

テキストファイルでは、文字コードの指定が重要です。

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

C#
using System.Text;

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

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

C#
using System.Text;

using var writer = new StreamWriter("output.txt", false, Encoding.UTF8);
writer.WriteLine("こんにちは");

文字化けが起きる場合は、ファイルの文字コードと読み書き時のEncodingが一致しているか確認しましょう。

4-5. File.ReadAllTextやFile.WriteAllTextとの使い分け

C#には、より簡単にテキストファイルを扱えるFile.ReadAllTextFile.WriteAllTextもあります。

C#
string text = File.ReadAllText("sample.txt");
File.WriteAllText("output.txt", text);

小さなファイルであれば、これらのメソッドで十分です。

一方、大きなファイルを少しずつ読みたい場合や、処理しながら書き込みたい場合は、StreamReaderStreamWriterを使うほうが適しています。

目安として、設定ファイルや短いログならFile.ReadAllText、大きなログやCSVを1行ずつ処理するならStreamReaderを使うとよいでしょう。

5. MemoryStreamでメモリ上のデータを読み書きする

5-1. MemoryStreamとは?一時データの保存に便利なStream

MemoryStreamは、メモリ上にデータを保持するStreamです。

ファイルを作らずに一時的なデータを扱いたい場合に便利です。

C#
using var memoryStream = new MemoryStream();

たとえば、Web APIへ送信するデータを一時的に作成したり、画像をメモリ上で加工したり、CSVをファイル保存せずに生成したりできます。

5-2. byte配列からMemoryStreamを作成する

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

C#
byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello MemoryStream");

using var stream = new MemoryStream(data);

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

string text = System.Text.Encoding.UTF8.GetString(buffer);
Console.WriteLine(text);

この方法は、すでにバイト配列として持っているデータをStreamとして扱いたいときに便利です。

5-3. MemoryStreamにデータを書き込む

MemoryStreamに文字列を書き込む例です。

C#
using System.Text;

using var stream = new MemoryStream();

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

書き込んだ後に読み込む場合は、Positionを先頭に戻します。

C#
stream.Position = 0;

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

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

Positionを戻し忘れると、読み込み位置が末尾のままになり、データを読み取れないことがあります。

5-4. ToArrayでMemoryStreamの内容を取り出す

MemoryStreamに書き込んだ内容は、ToArraybyte[]として取り出せます。

C#
using var stream = new MemoryStream();

byte[] data = System.Text.Encoding.UTF8.GetBytes("Sample Data");
stream.Write(data, 0, data.Length);

byte[] result = stream.ToArray();

ToArrayは、MemoryStreamの現在の内容を新しいバイト配列としてコピーします。

文字列に戻す場合は、次のようにします。

C#
string text = System.Text.Encoding.UTF8.GetString(result);
Console.WriteLine(text);

5-5. ファイルを使わずに画像・CSV・JSONを扱う活用例

MemoryStreamは、ファイルを作らずにデータを一時生成したい場面でよく使われます。

たとえば、CSVデータをメモリ上で作る場合は次のように書けます。

C#
using System.Text;

using var stream = new MemoryStream();
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);

writer.WriteLine("Id,Name");
writer.WriteLine("1,Tanaka");
writer.WriteLine("2,Suzuki");
writer.Flush();

byte[] csvBytes = stream.ToArray();

ここでleaveOpen: trueを指定しているのは、StreamWriterを破棄しても元のMemoryStreamを閉じないようにするためです。

JSONをメモリ上で扱う場合も同様です。

C#
string json = "{\"name\":\"Tanaka\",\"age\":30}";
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);

using var stream = new MemoryStream(jsonBytes);

ファイル保存が不要な処理では、MemoryStreamを使うと効率的です。

6. Streamでバイナリデータを扱う方法

6-1. byte配列とStreamの関係

Streamは基本的にbyteの並びを読み書きします。

文字列も、画像も、PDFも、最終的にはバイト列として扱われます。テキストの場合は、Encoding.UTF8などを使って文字列とバイト配列を変換します。

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

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

StreamのReadWriteは、このbyte[]を使ってデータをやり取りします。

C#
stream.Write(bytes, 0, bytes.Length);

6-2. BinaryReaderでバイナリデータを読み込む

BinaryReaderを使うと、整数や文字列などをバイナリ形式で読み込めます。

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

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

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

BinaryReaderは、ReadInt32ReadDoubleReadBooleanなど、型ごとに読み込むメソッドを持っています。

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

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

BinaryWriterを使うと、データをバイナリ形式で書き込めます。

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

writer.Write(1);
writer.Write("Tanaka");
writer.Write(true);

このように書き込んだファイルは、テキストエディタで開いても意味のある文字列としては読めません。

読み込むときは、同じ順番で読み取ります。

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

int id = reader.ReadInt32();
string name = reader.ReadString();
bool active = reader.ReadBoolean();

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

テキストデータは、人間が読める文字列として表現されたデータです。

1,Tanaka,true

一方、バイナリデータは、コンピューターが扱いやすい形式で保存されたデータです。画像、音声、動画、PDF、ZIPなどはバイナリデータです。

テキストデータはStreamReaderStreamWriterで扱いやすく、バイナリデータはStreamBinaryReaderBinaryWriterで扱うのが基本です。

6-5. 画像・PDF・圧縮ファイルを扱うときの注意点

画像、PDF、ZIPなどのファイルはバイナリデータです。文字列として読み込むとデータが壊れる可能性があります。

たとえば、PDFを次のようにStreamReaderで読み込むのは適切ではありません。

C#
// PDFや画像には不向き
using var reader = new StreamReader("sample.pdf");
string text = reader.ReadToEnd();

バイナリファイルは、byte配列やStreamとして扱いましょう。

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

大容量ファイルでは、ReadAllBytesで一度に読み込むのではなく、Streamで分割して処理するのがおすすめです。

7. C# Streamの非同期処理

7-1. ReadAsyncとWriteAsyncを使うメリット

C#では、Streamの読み書きを非同期で行うためにReadAsyncWriteAsyncを使えます。

非同期処理を使うと、ファイルやネットワークの読み書き中にスレッドを待機で占有しにくくなります。

特に、WebアプリケーションやGUIアプリケーションでは、I/O処理を非同期にすることで応答性を保ちやすくなります。

C#
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
C#
await stream.WriteAsync(buffer, 0, buffer.Length);

7-2. async・awaitでファイルを非同期に読み込む

ファイルを非同期で読み込む例です。

C#
using System.Text;

static async Task ReadFileAsync()
{
using var stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);

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 var stream = new FileStream("largefile.dat", FileMode.Open, FileAccess.Read);

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

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

7-3. async・awaitでファイルへ非同期に書き込む

ファイルへ非同期で書き込む例です。

C#
using System.Text;

static async Task WriteFileAsync()
{
string text = "非同期で書き込みます";
byte[] data = Encoding.UTF8.GetBytes(text);

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

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

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

C#
static async Task WriteTextAsync()
{
using var writer = new StreamWriter("output.txt");

await writer.WriteLineAsync("1行目");
await writer.WriteLineAsync("2行目");
}

テキストを扱う場合は、StreamWriter.WriteLineAsyncを使うと読みやすいコードになります。

7-4. 大容量ファイル処理で非同期処理が有効な理由

大容量ファイルを処理すると、読み書きに時間がかかります。

同期処理では、読み書きが終わるまで現在のスレッドが待機します。GUIアプリでは画面が固まったように見えることがあります。Webアプリでは、リクエスト処理に使うスレッドを長時間占有してしまう可能性があります。

非同期処理を使うと、I/O待ちの間に他の処理を進めやすくなります。

ただし、非同期にすれば必ず処理速度が速くなるわけではありません。非同期処理の主なメリットは、待機中のスレッドを効率よく使えることです。

7-5. ConfigureAwaitやCancellationTokenを使う場面

ライブラリコードでは、ConfigureAwait(false)を使うことがあります。

C#
await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);

これは、await後に元の同期コンテキストへ戻る必要がない場合に使われます。アプリケーションコードでは必須ではありませんが、ライブラリ開発ではよく使われます。

また、処理をキャンセルできるようにしたい場合は、CancellationTokenを使います。

C#
static async Task CopyFileAsync(string source, string destination, CancellationToken cancellationToken)
{
using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
using var output = new FileStream(destination, FileMode.Create, FileAccess.Write);

await input.CopyToAsync(output, cancellationToken);
}

大容量ファイルのコピーやダウンロード処理では、キャンセル対応を入れておくと実用的です。

8. Streamをコピー・変換する方法

8-1. CopyToでStreamからStreamへコピーする

Stream同士のコピーには、CopyToを使います。

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

input.CopyTo(output);

CopyToを使うと、バッファを自分で用意してループを書く必要がありません。

ファイルのコピーだけでなく、MemoryStreamへのコピーにも使えます。

C#
using var input = File.OpenRead("sample.txt");
using var memory = new MemoryStream();

input.CopyTo(memory);

8-2. CopyToAsyncで非同期にコピーする

非同期でStreamをコピーする場合は、CopyToAsyncを使います。

C#
static async Task CopyAsync()
{
using var input = new FileStream("source.txt", FileMode.Open, FileAccess.Read);
using var output = new FileStream("copy.txt", FileMode.Create, FileAccess.Write);

await input.CopyToAsync(output);
}

Webアプリケーションでファイルアップロードやダウンロードを扱う場合は、CopyToAsyncがよく使われます。

キャンセル対応を入れる場合は、CancellationTokenを渡します。

C#
await input.CopyToAsync(output, cancellationToken);

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

Streamをbyte配列に変換するには、MemoryStreamへコピーしてToArrayを使います。

C#
static byte[] StreamToBytes(Stream stream)
{
using var memory = new MemoryStream();
stream.CopyTo(memory);
return memory.ToArray();
}

ただし、この方法はStream全体をメモリに読み込むため、大容量データには向きません。

すでにMemoryStreamの場合は、そのままToArrayを使えます。

C#
byte[] bytes = memoryStream.ToArray();

読み込み位置が途中になっているStreamを先頭から読みたい場合は、可能であればPositionを戻します。

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

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

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

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

using var stream = new MemoryStream(data);

この方法により、byte配列を受け取る処理とStreamを受け取る処理をつなげられます。

たとえば、次のようなメソッドに渡せます。

C#
void Process(Stream stream)
{
// Streamとして処理
}

byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello");
using var stream = new MemoryStream(data);

Process(stream);

8-5. Streamと文字列を相互変換する

文字列をStreamに変換するには、文字列をbyte配列にしてMemoryStreamを作ります。

C#
using System.Text;

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

using var stream = new MemoryStream(bytes);

Streamから文字列へ変換する場合は、StreamReaderを使うと簡単です。

C#
using var reader = new StreamReader(stream, Encoding.UTF8);
string text = reader.ReadToEnd();

ただし、読み込み前にStreamの位置が末尾になっている場合は、先頭に戻す必要があります。

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

MemoryStreamに文字列を書き込んで、再び読み出す例です。

C#
using System.Text;

using var stream = new MemoryStream();
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);

writer.Write("こんにちは");
writer.Flush();

stream.Position = 0;

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

Console.WriteLine(result);

9. Streamを使うときの注意点とよくあるエラー

9-1. Dispose忘れによるファイルロックやメモリリーク

Streamを使うときは、Dispose忘れに注意が必要です。

特にFileStreamでは、Disposeを忘れるとファイルがロックされたままになることがあります。

C#
var stream = new FileStream("sample.txt", FileMode.Open);
// Disposeされないと問題になる可能性がある

基本的には、次のようにusingを使いましょう。

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

usingを使えば、例外が発生しても自動的にDisposeされるため安全です。

9-2. Positionを戻さずに読み込んで空になるケース

MemoryStreamで特によくあるのが、書き込み後に読み込んでも空になるケースです。

C#
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream, leaveOpen: true);

writer.Write("Hello");
writer.Flush();

// Positionが末尾のまま
using var reader = new StreamReader(stream);
string text = reader.ReadToEnd();

Console.WriteLine(text); // 空になる

書き込み後に読み込む場合は、Positionを先頭に戻します。

C#
stream.Position = 0;

修正後のコードです。

C#
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream, leaveOpen: true);

writer.Write("Hello");
writer.Flush();

stream.Position = 0;

using var reader = new StreamReader(stream);
string text = reader.ReadToEnd();

Console.WriteLine(text);

9-3. Streamが閉じられていてObjectDisposedExceptionになる原因

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

C#
MemoryStream stream;

using (stream = new MemoryStream())
{
byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello");
stream.Write(data, 0, data.Length);
}

// ここではstreamはすでにDisposeされている
stream.Position = 0;

また、StreamReaderStreamWriterをDisposeすると、既定では元のStreamも閉じられます。

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

C#
using var stream = new MemoryStream();

using (var writer = new StreamWriter(stream, leaveOpen: true))
{
writer.Write("Hello");
writer.Flush();
}

stream.Position = 0;

9-4. 読み書き権限がなくUnauthorizedAccessExceptionになる原因

UnauthorizedAccessExceptionは、ファイルやフォルダへのアクセス権限がない場合に発生します。

たとえば、フォルダをファイルとして開こうとした場合や、書き込み権限のない場所にファイルを作ろうとした場合です。

C#
try
{
using var stream = new FileStream(@"C:\protected\sample.txt", FileMode.Create);
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("アクセス権限がありません。");
}

対策としては、保存先のパスを見直す、アプリケーションの実行権限を確認する、読み取り専用ファイルではないか確認する、といった方法があります。

9-5. 大容量データを一度に読み込まないための考え方

大容量ファイルを扱うときに、次のようなコードは注意が必要です。

C#
byte[] data = File.ReadAllBytes("largefile.dat");

この方法はファイル全体をメモリに読み込みます。数GBのファイルではメモリ不足になる可能性があります。

大容量ファイルでは、一定サイズのバッファを使って少しずつ処理します。

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

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

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

この考え方は、ファイルコピー、アップロード、ダウンロード、ログ解析などで重要です。

10. C# Streamの種類と使い分け

10-1. FileStreamはファイル操作に使う

FileStreamは、ファイルをバイト単位で読み書きするために使います。

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

画像、PDF、ZIPなどのバイナリファイルを扱う場合や、大容量ファイルを分割して処理したい場合に適しています。

テキストファイルを文字列として扱う場合は、FileStreamStreamReaderを組み合わせることもできます。

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

string text = reader.ReadToEnd();

10-2. MemoryStreamはメモリ上の一時処理に使う

MemoryStreamは、メモリ上で一時的にデータを扱うために使います。

C#
using var stream = new MemoryStream();

ファイルを作らずにデータを生成したい場合や、byte配列をStreamとして扱いたい場合に便利です。

ただし、大きすぎるデータをMemoryStreamに入れるとメモリを大量に消費します。大容量データにはFileStreamや分割処理を使いましょう。

10-3. NetworkStreamは通信データの送受信に使う

NetworkStreamは、ネットワーク通信でデータを送受信するためのStreamです。

TCP通信などで使われます。

C#
using var client = new System.Net.Sockets.TcpClient("example.com", 80);
using NetworkStream stream = client.GetStream();

NetworkStreamは、ファイルのように自由に位置を移動できません。そのため、CanSeekは通常falseです。

ネットワーク通信では、受信できるデータ量がタイミングによって変わるため、必要なデータがそろうまで繰り返し読み込む設計が重要です。

10-4. BufferedStreamは読み書きを効率化したいときに使う

BufferedStreamは、他のStreamにバッファ機能を追加するためのStreamです。

C#
using var fileStream = new FileStream("sample.dat", FileMode.Open);
using var bufferedStream = new BufferedStream(fileStream);

細かい読み書きが多い場合、バッファを使うことでI/O回数を減らし、効率化できる場合があります。

ただし、FileStream自体にもバッファ機能があるため、常にBufferedStreamが必要というわけではありません。細かい読み書きが多く、性能改善が必要な場合に検討するとよいでしょう。

10-5. CryptoStreamやGZipStreamなど応用的なStream

C#には、Streamを組み合わせて使う応用的なクラスもあります。

CryptoStreamは暗号化や復号に使います。

GZipStreamはGZip形式の圧縮・展開に使います。

たとえば、GZipで圧縮する例です。

C#
using System.IO.Compression;

using var input = File.OpenRead("sample.txt");
using var output = File.Create("sample.txt.gz");
using var gzip = new GZipStream(output, CompressionMode.Compress);

input.CopyTo(gzip);

Streamはこのように、データの流れに別の処理を重ねる形で使えるのが大きな特徴です。

11. 実践例で学ぶC# Streamの活用パターン

11-1. 大容量ファイルを分割して読み込む

大容量ファイルを扱う場合は、バッファを使って分割して読み込みます。

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

byte[] buffer = new byte[1024 * 1024];
int bytesRead;
long totalBytes = 0;

while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytes += bytesRead;
Console.WriteLine($"{totalBytes}バイト処理しました");
}

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

ログ解析、動画ファイル処理、バックアップ処理などでよく使われる考え方です。

11-2. CSVファイルをStreamReaderで読み込む

CSVファイルを1行ずつ読み込む例です。

C#
using var reader = new StreamReader("users.csv");

string? line;
while ((line = reader.ReadLine()) != null)
{
string[] columns = line.Split(',');

string id = columns[0];
string name = columns[1];

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

単純なCSVであればこのように処理できます。

ただし、実際のCSVでは、カンマを含む値やダブルクォーテーションを含む値があります。本格的なCSV処理では、CSVパーサーのライブラリを使うほうが安全です。

11-3. MemoryStreamを使ってファイルを一時生成する

MemoryStreamを使えば、ファイルを保存せずに一時的なデータを生成できます。

C#
using System.Text;

static byte[] CreateCsv()
{
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);

writer.WriteLine("Id,Name");
writer.WriteLine("1,Tanaka");
writer.WriteLine("2,Suzuki");
writer.Flush();

return stream.ToArray();
}

このような処理は、WebアプリでCSVファイルをダウンロードさせる場合や、外部APIへファイルとして送信する場合に便利です。

11-4. Web APIのレスポンスをStreamで処理する

HTTPレスポンスの内容をStreamとして処理することもできます。

C#
using var httpClient = new HttpClient();

using Stream stream = await httpClient.GetStreamAsync("https://example.com/data.json");
using var reader = new StreamReader(stream);

string json = await reader.ReadToEndAsync();

Console.WriteLine(json);

大きなレスポンスを扱う場合は、すべてを文字列にせず、Streamとして処理することでメモリ使用量を抑えられます。

ファイルをダウンロードする場合は、次のようにStream同士でコピーできます。

C#
using var httpClient = new HttpClient();

using Stream input = await httpClient.GetStreamAsync("https://example.com/file.zip");
using var output = File.Create("file.zip");

await input.CopyToAsync(output);

11-5. ファイルアップロード・ダウンロード処理で使う

Webアプリケーションでは、ファイルアップロードやダウンロードでStreamがよく使われます。

アップロードされたファイルを保存するイメージは次のようになります。

C#
using var output = new FileStream("uploaded.dat", FileMode.Create);

await uploadedStream.CopyToAsync(output);

ダウンロード処理では、ファイルをStreamとして返す設計がよく使われます。

C#
using var stream = new FileStream("report.pdf", FileMode.Open, FileAccess.Read);

ファイルサイズが大きい場合でも、Streamを使えば少しずつ読み書きできるため、メモリ効率のよい処理を実装できます。

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

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

Streamは、データの読み書きを表す抽象クラスです。

FileStreamは、ファイルを読み書きするためにStreamを継承した具体的なクラスです。

つまり、Streamは共通の仕組み、FileStreamはファイル用の実装です。

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

このように、FileStreamはStream型の変数に代入できます。

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

テキストファイルを文字列として読みたいなら、StreamReaderを使うのがおすすめです。

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

画像、PDF、ZIPなどのバイナリファイルを扱う場合は、FileStreamを使います。

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

テキストならStreamReader、バイナリならFileStreamと考えるとわかりやすいです。

12-3. MemoryStreamはDisposeする必要がある?

MemoryStreamはメモリ上のデータを扱うため、FileStreamのようにファイルハンドルを持つわけではありません。

それでも、Streamを扱う習慣としてDisposeするのがおすすめです。

C#
using var stream = new MemoryStream();

特に、Streamを受け取る汎用的なコードでは、FileStreamなのかMemoryStreamなのかを意識しないことも多いため、使い終わったらDisposeするというルールにしておくと安全です。

12-4. Streamを使うと処理速度は速くなる?

Streamを使えば必ず速くなるわけではありません。

Streamのメリットは、データを一度にすべて読み込まず、少しずつ処理できることです。これにより、大容量ファイルでもメモリ使用量を抑えられます。

小さなファイルであれば、File.ReadAllTextFile.ReadAllBytesのほうが簡単で十分な場合もあります。

一方、大容量ファイル、ネットワーク通信、ファイルアップロード・ダウンロードでは、Streamを使う設計が有効です。

12-5. 大きなファイルを扱うときのおすすめ実装は?

大きなファイルを扱うときは、一度にすべて読み込まず、バッファを使って分割処理するのがおすすめです。

C#
using var input = new FileStream("largefile.dat", FileMode.Open, FileAccess.Read);
using var output = new FileStream("copy.dat", FileMode.Create, FileAccess.Write);

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

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

非同期処理にする場合は、次のように書けます。

C#
using var input = new FileStream("largefile.dat", FileMode.Open, FileAccess.Read);
using var output = new FileStream("copy.dat", FileMode.Create, FileAccess.Write);

await input.CopyToAsync(output);

単純なコピーであればCopyToCopyToAsyncを使うと、簡潔で読みやすいコードになります。

まとめ

C#のStreamは、ファイル、メモリ、ネットワークなど、さまざまなデータの読み書きを共通の方法で扱うための重要な仕組みです。

FileStreamはファイル操作、MemoryStreamはメモリ上の一時データ、NetworkStreamは通信データの送受信に使います。テキストファイルを扱う場合はStreamReaderStreamWriter、バイナリデータを扱う場合はBinaryReaderBinaryWriterも便利です。

Streamを使うときは、ReadWriteSeekPositionLengthFlushDisposeといった基本操作を理解しておくことが大切です。特に、Positionの戻し忘れやDispose忘れは初心者がつまずきやすいポイントです。

また、大容量ファイルを扱う場合は、一度にすべて読み込まず、バッファを使って分割処理するのが基本です。非同期処理ではReadAsyncWriteAsyncCopyToAsyncを使うことで、I/O待ちを効率よく扱えます。

C# Streamを理解しておくと、ファイル読み書きだけでなく、Web API、ファイルアップロード、ダウンロード、画像やPDFの処理など、実践的な開発で役立つ場面が大きく広がります。