C# FileStreamの使い方完全ガイド|ファイル読み書き・using・例外処理まで初心者向けに解説
はじめに
C#でファイルの読み書きを行うとき、基本となるクラスのひとつがFileStreamです。csharp filestreamで調べると、FileStream、FileMode、FileAccess、using、StreamReader、StreamWriterなど多くの用語が出てきて、初心者には少し難しく感じるかもしれません。
FileStreamは、ファイルを「バイトの流れ」として扱うためのクラスです。テキストファイルだけでなく、画像、CSV、PDF、独自形式のバイナリファイルなども扱えます。Microsoft Learnでも、FileStreamは同期・非同期の読み取りと書き込みをサポートするファイル用のStreamとして説明されています。
この記事では、C#のFileStreamの基本から、ファイル読み込み、書き込み、usingによる安全な解放、例外処理、非同期処理、よくあるトラブルまで、初心者向けに順番に解説します。
1. C# FileStreamとは?ファイル読み書きで使う基本クラス
1-1. FileStreamの役割とできること
FileStreamは、C#でファイルを直接読み書きするためのクラスです。ファイルの中身をbyte単位で扱うため、テキストファイルだけでなく、画像や音声、動画、ZIPファイルのようなバイナリファイルにも利用できます。
たとえば、次のような処理に使えます。
ファイルを開く
ファイルからバイトデータを読み込む
ファイルへバイトデータを書き込む
既存ファイルに追記する
大きなファイルを少しずつ処理する
他のプロセスとのファイル共有を制御する
非同期でファイルを読み書きする
FileStreamは低レベル寄りのファイル操作クラスなので、「ファイルを細かく制御したい」「大きなファイルを効率よく扱いたい」という場面で特に役立ちます。
1-2. Stream・File・FileInfoとの違い
C#のファイル操作では、FileStream以外にもStream、File、FileInfoなどが登場します。それぞれの違いを整理すると、次のようになります。
| クラス | 役割 | 特徴 |
|---|---|---|
Stream | データの流れを表す抽象クラス | ファイル、メモリ、ネットワークなどの共通基盤 |
FileStream | ファイル用のStream | ファイルをバイト単位で読み書きできる |
File | ファイル操作用の静的クラス | ReadAllTextやWriteAllTextなどを簡単に使える |
FileInfo | ファイル情報を表すインスタンスクラス | ファイルサイズ、更新日時、存在確認などを扱いやすい |
簡単なテキスト読み書きならFile.ReadAllTextやFile.WriteAllTextが便利です。一方で、ファイルを少しずつ読みたい、共有モードを指定したい、バイナリデータを扱いたい場合はFileStreamが向いています。
1-3. FileStreamを使うべきケース
FileStreamを使うべき代表的なケースは次のとおりです。
1つ目は、大容量ファイルを扱う場合です。File.ReadAllBytesやFile.ReadAllTextはファイル全体をメモリに読み込むため、サイズが大きいとメモリを圧迫します。FileStreamなら、バッファに分割して少しずつ読み書きできます。
2つ目は、バイナリファイルを扱う場合です。画像、音声、動画、独自フォーマットのファイルなどは文字列ではなくbyte[]として処理するため、FileStreamが適しています。
3つ目は、ファイルの開き方を細かく制御したい場合です。FileMode、FileAccess、FileShareを指定することで、「新規作成するのか」「読み取り専用なのか」「他プロセスの読み取りを許可するのか」などを制御できます。
1-4. 初心者がつまずきやすいポイント
初心者がFileStreamでつまずきやすいポイントは、主に次の5つです。
まず、FileStreamは文字列ではなくバイトを扱うという点です。テキストを読み書きする場合は、文字列をEncoding.UTF8.GetBytesでbyte[]に変換したり、StreamReaderやStreamWriterと組み合わせたりします。
次に、ファイルを閉じ忘れる問題です。FileStreamを閉じないと、ファイルがロックされたままになり、他の処理から開けなくなることがあります。そのため、基本的にはusingを使って自動的に解放します。
また、FileMode.CreateとFileMode.Openの違いも重要です。Createは既存ファイルを上書きする可能性があり、Openはファイルが存在しないと例外になります。FileModeはファイルを上書きするか、作成するか、開くかなどを制御する列挙型です。
2. FileStreamを使うための基本準備
2-1. 必要な名前空間 System.IO
FileStreamを使うには、基本的にSystem.IO名前空間を使用します。
C#using System;
using System.IO;
using System.Text;
FileStream、FileMode、FileAccess、FileShare、StreamReader、StreamWriterなどはSystem.IOに含まれています。文字コードを扱う場合はSystem.Textも使います。
2-2. FileStreamの基本構文
FileStreamの基本構文は次のようになります。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None
);
主な引数は次のとおりです。
| 引数 | 意味 |
|---|---|
| 第1引数 | ファイルパス |
| 第2引数 | ファイルの開き方 |
| 第3引数 | 読み取り・書き込み権限 |
| 第4引数 | 他のプロセスとの共有設定 |
最初は難しく見えますが、「どのファイルを」「どの方法で開き」「読み書きのどちらを行い」「他からのアクセスを許可するか」を指定しているだけです。
2-3. ファイルパスの指定方法
ファイルパスは、読み書きしたいファイルの場所を表します。
C#string path = "sample.txt";
このようにファイル名だけを書くと、実行中のアプリケーションの作業ディレクトリにあるsample.txtを対象にします。
フォルダを含めて指定することもできます。
C#string path = @"C:\work\sample.txt";
Windowsでは\がパス区切り文字として使われますが、C#では\がエスケープ文字でもあるため、文字列の前に@を付けると書きやすくなります。
2-4. 相対パスと絶対パスの違い
相対パスは、現在の実行場所を基準にしたパスです。
C#string path = "data/sample.txt";
絶対パスは、ドライブ名やルートから完全に指定するパスです。
C#string path = @"C:\work\data\sample.txt";
相対パスは環境が変わると参照先も変わる可能性があります。初心者が「ファイルパスは正しいはずなのに見つからない」と困る場合、実行時のカレントディレクトリが想定と違っていることがよくあります。
確認したい場合は、次のコードで現在の作業ディレクトリを表示できます。
C#Console.WriteLine(Directory.GetCurrentDirectory());
2-5. Windows環境でのパス指定時の注意点
Windows環境でパスを書くときは、次の点に注意しましょう。
C#// OK:逐語的文字列リテラル
string path1 = @"C:\work\sample.txt";
// OK:バックスラッシュをエスケープ
string path2 = "C:\\work\\sample.txt";
// NGになりやすい:\w などが意図しない解釈になる可能性
string path3 = "C:\work\sample.txt";
また、フォルダが存在しない状態でファイルを作成しようとすると、DirectoryNotFoundExceptionが発生します。ファイルを作成する前に、必要に応じてDirectory.CreateDirectoryでフォルダを作成しておくと安全です。
C#string directory = @"C:\work\data";
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, "sample.txt");
Path.Combineを使うと、区切り文字を自分で意識せずにパスを組み立てられます。
3. FileStreamのコンストラクタと主要な引数
3-1. FileModeとは
FileModeは、ファイルをどのように開くかを指定する列挙型です。代表的な値は次のとおりです。
| FileMode | 意味 |
|---|---|
CreateNew | 新規作成する。既に存在すると例外 |
Create | 新規作成する。既に存在すると上書き |
Open | 既存ファイルを開く。存在しないと例外 |
OpenOrCreate | 存在すれば開き、なければ作成 |
Truncate | 既存ファイルを開き、内容を0バイトにする |
Append | 末尾に追記する |
FileMode.Createは既存ファイルを上書きするため、誤って重要なファイルを消さないように注意が必要です。FileMode.Appendはファイル末尾に書き込む用途で使います。
3-2. FileAccessとは
FileAccessは、ファイルに対して読み取りを行うのか、書き込みを行うのか、その両方を行うのかを指定します。Microsoft Learnでは、Read、Write、ReadWriteが定義されています。
| FileAccess | 意味 |
|---|---|
Read | 読み取り専用 |
Write | 書き込み専用 |
ReadWrite | 読み取りと書き込み |
読み込みだけならFileAccess.Read、書き込みだけならFileAccess.Writeを指定します。必要以上にReadWriteを使わないほうが、意図しない変更を防ぎやすくなります。
3-3. FileShareとは
FileShareは、自分がファイルを開いている間、他の処理が同じファイルを開けるかどうかを指定します。Noneを指定すると共有を拒否し、ファイルが閉じられるまで他からのオープン要求は失敗します。ReadやReadWriteを指定すると、後続の読み取りや読み書きのオープンを許可できます。
| FileShare | 意味 |
|---|---|
None | 他から開くことを許可しない |
Read | 他からの読み取りを許可 |
Write | 他からの書き込みを許可 |
ReadWrite | 他からの読み取り・書き込みを許可 |
Delete | 他からの削除を許可 |
ログファイルのように、他のプロセスが読み取る可能性があるファイルではFileShare.ReadやFileShare.ReadWriteを検討します。
3-4. bufferSizeとは
bufferSizeは、FileStreamが内部的に使うバッファのサイズです。
C#using FileStream fs = new FileStream(
"large.dat",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 81920
);
バッファサイズを大きくすると、読み書きの回数を減らせる場合があります。ただし、大きくすれば必ず速くなるわけではありません。一般的な処理では既定値や4096、大きなファイルコピーでは81920などがよく使われます。
3-5. useAsyncとは
useAsyncは、非同期I/Oを使うかどうかを指定する引数です。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
useAsync: true
);
ReadAsyncやWriteAsyncを使う場合は、useAsync: trueを指定しておくと非同期処理に適した形でファイルを開けます。近年の.NETではFileStreamOptionsを使って、モード、アクセス権、共有、バッファサイズ、オプションをまとめて指定する書き方もあります。
C#var options = new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
BufferSize = 4096,
Options = FileOptions.Asynchronous
};
using FileStream fs = new FileStream("sample.txt", options);
3-6. よく使うFileStream生成パターン
読み取り専用で開く場合です。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
新規作成または上書きして書き込む場合です。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Create,
FileAccess.Write,
FileShare.None
);
既存ファイルに追記する場合です。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Append,
FileAccess.Write,
FileShare.Read
);
読み書き両方を行う場合です。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.None
);
4. FileStreamでファイルを読み込む方法
4-1. byte配列に読み込む基本コード
FileStreamでファイルを読み込む基本コードは次のとおりです。
C#using System;
using System.IO;
string path = "sample.txt";
using FileStream fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
byte[] buffer = new byte[fs.Length];
int bytesRead = fs.Read(buffer, 0, buffer.Length);
Console.WriteLine($"{bytesRead} バイト読み込みました");
このコードでは、ファイルサイズ分のbyte[]を作成し、Readメソッドで読み込んでいます。ただし、大きなファイルではファイル全体を一度に配列へ読み込むとメモリを多く使うため、分割読み込みが推奨されます。
4-2. Readメソッドの使い方
Readメソッドは、ストリームからバイトブロックを読み取り、指定したバッファに格納します。戻り値は実際に読み込んだバイト数です。
C#int readBytes = fs.Read(buffer, offset, count);
| 引数 | 意味 |
|---|---|
buffer | 読み込んだデータを入れる配列 |
offset | 配列のどの位置から書き込むか |
count | 最大何バイト読み込むか |
Readは、必ず指定したcount分を読み込むとは限りません。そのため、大きなファイルやストリーム処理では、戻り値が0になるまでループするのが基本です。
C#byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// bufferの先頭からbytesReadバイト分を処理する
Console.WriteLine($"{bytesRead} バイト読み込みました");
}
4-3. テキストファイルを読み込む方法
FileStreamはバイトを扱うため、テキストとして読むには文字コードを使って文字列に変換します。
C#using System;
using System.IO;
using System.Text;
string path = "sample.txt";
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[fs.Length];
fs.Read(buffer, 0, buffer.Length);
string text = Encoding.UTF8.GetString(buffer);
Console.WriteLine(text);
この例では、UTF-8として読み込んでいます。日本語の文字化けを防ぐには、書き込み時と読み込み時の文字コードを合わせることが重要です。
4-4. StreamReaderと組み合わせる方法
テキストファイルを読む場合は、FileStreamを直接使うより、StreamReaderと組み合わせるほうが簡単です。
C#using System;
using System.IO;
using System.Text;
string path = "sample.txt";
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
string text = reader.ReadToEnd();
Console.WriteLine(text);
1行ずつ読み込む場合は、次のように書けます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
テキスト処理ではStreamReaderを使うことで、文字コードや行単位の読み取りを扱いやすくなります。
4-5. 大きなファイルを効率よく読み込むコツ
大きなファイルを扱う場合は、ファイル全体を一度に読み込まず、一定サイズのバッファで分割して処理します。
C#string path = "large.dat";
byte[] buffer = new byte[81920];
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// bytesReadバイト分だけ処理する
}
ポイントは、buffer.Length全体ではなく、実際に読み込んだbytesRead分だけ処理することです。最後の読み込みでは、バッファサイズより少ないバイト数になることがあります。
5. FileStreamでファイルを書き込む方法
5-1. byte配列を書き込む基本コード
FileStreamでファイルに書き込む基本コードは次のとおりです。
C#using System.IO;
using System.Text;
string path = "output.txt";
string text = "こんにちは、FileStream!";
byte[] data = Encoding.UTF8.GetBytes(text);
using FileStream fs = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.None
);
fs.Write(data, 0, data.Length);
このコードでは、文字列をUTF-8のbyte[]に変換してから、Writeメソッドでファイルへ書き込んでいます。
5-2. Writeメソッドの使い方
Writeメソッドは、指定したバイト配列の内容をファイルに書き込みます。
C#fs.Write(buffer, offset, count);
| 引数 | 意味 |
|---|---|
buffer | 書き込むデータが入った配列 |
offset | 配列のどの位置から書き込むか |
count | 何バイト書き込むか |
たとえば、配列全体を書き込む場合は次のようにします。
C#byte[] data = Encoding.UTF8.GetBytes("C# FileStreamの書き込みテスト");
fs.Write(data, 0, data.Length);
5-3. テキストファイルを書き込む方法
FileStreamだけでテキストを書き込む場合は、文字列をbyte[]へ変換します。
C#using System.IO;
using System.Text;
string path = "message.txt";
string text = "テキストを書き込みます。";
byte[] bytes = Encoding.UTF8.GetBytes(text);
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
fs.Write(bytes, 0, bytes.Length);
この方法でも問題ありませんが、テキストを扱うならStreamWriterを使うほうが自然です。
5-4. StreamWriterと組み合わせる方法
StreamWriterを使うと、文字列をそのまま書き込めます。
C#using System.IO;
using System.Text;
string path = "message.txt";
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);
writer.WriteLine("1行目");
writer.WriteLine("2行目");
writer.WriteLine("C# FileStreamとStreamWriterの組み合わせです。");
StreamWriterは文字列を扱うためのクラスなので、CSVやログファイルなどのテキスト出力に向いています。
5-5. 上書き・追記・新規作成の使い分け
ファイル書き込みでは、FileModeの選び方が非常に重要です。
上書きする場合はFileMode.Createを使います。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Create, FileAccess.Write);
既存ファイルがある場合に例外にしたいならFileMode.CreateNewを使います。
C#using FileStream fs = new FileStream("sample.txt", FileMode.CreateNew, FileAccess.Write);
追記する場合はFileMode.Appendを使います。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Append, FileAccess.Write);
存在すれば開き、なければ作る場合はFileMode.OpenOrCreateを使います。
C#using FileStream fs = new FileStream("sample.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);
初心者が特に注意したいのは、FileMode.Createが既存ファイルを上書きする点です。消えて困るファイルには、CreateNewや存在チェックを組み合わせましょう。
5-6. Flushメソッドの役割
Flushメソッドは、バッファに残っているデータをファイルへ反映するためのメソッドです。
C#fs.Write(data, 0, data.Length);
fs.Flush();
通常はusingでFileStreamを閉じるときに必要な処理が行われます。そのため、毎回明示的にFlushを呼ぶ必要はありません。ただし、ファイルを開いたまま途中経過を確実に反映したい場合にはFlushが役立ちます。
6. usingでFileStreamを安全に扱う方法
6-1. FileStreamを閉じ忘れると起きる問題
FileStreamはファイルハンドルというOSリソースを使います。閉じ忘れると、次のような問題が起きる可能性があります。
ファイルが使用中のままになる
他の処理から開けない
書き込み内容が反映されないことがある
リソースを無駄に消費する
例外発生時に後始末が行われない
そのため、FileStreamを使うときは、基本的にusingで囲むのがベストプラクティスです。
6-2. using文の基本
using文を使うと、ブロックを抜けるときに自動的にDisposeが呼ばれます。Microsoft Learnでも、using文はブロック内で例外が発生した場合でも破棄可能なインスタンスを破棄することを保証すると説明されています。
C#using (FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read))
{
// ファイルを読み込む処理
}
このブロックを抜けると、fs.Dispose()が自動的に呼ばれ、ファイルが閉じられます。
6-3. using宣言の書き方
C# 8.0以降では、波かっこを使わないusing宣言も使えます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
// このスコープの終わりで自動的にDisposeされる
コードが短くなり、ネストも減らせます。ただし、どのスコープの終わりで破棄されるのかを意識する必要があります。
6-4. DisposeとCloseの違い
Closeはストリームを閉じるためのメソッドです。一方、Disposeは使い終わったリソースを解放するための仕組みです。FileStreamでは、Disposeによって内部的にファイルハンドルが解放されます。
基本的には、自分でCloseを呼ぶよりもusingに任せるほうが安全です。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open);
// 処理を書く
// スコープ終了時に自動でDisposeされる
IDisposableは、ファイルやハンドルなどのアンマネージリソースを明示的に解放するために使われる仕組みです。
6-5. usingを使った読み書きサンプル
usingを使って、安全にファイルへ書き込む例です。
C#using System.IO;
using System.Text;
string path = "sample.txt";
string text = "usingで安全にFileStreamを扱います。";
byte[] data = Encoding.UTF8.GetBytes(text);
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
{
fs.Write(data, 0, data.Length);
}
読み込む例です。
C#using System;
using System.IO;
using System.Text;
string path = "sample.txt";
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
string text = Encoding.UTF8.GetString(data);
Console.WriteLine(text);
}
7. FileStreamの例外処理とエラー対策
7-1. FileNotFoundException
FileNotFoundExceptionは、指定したファイルが存在しない場合に発生します。
C#using FileStream fs = new FileStream("notfound.txt", FileMode.Open, FileAccess.Read);
FileMode.Openは既存ファイルを開くため、ファイルが存在しないと例外になります。存在しない場合に作成したいならFileMode.OpenOrCreateを使います。
7-2. DirectoryNotFoundException
DirectoryNotFoundExceptionは、指定したパス内のフォルダが存在しない場合に発生します。
C#using FileStream fs = new FileStream(@"C:\no_such_folder\sample.txt", FileMode.Create);
対策として、ファイル作成前にディレクトリを作成します。
C#string directory = @"C:\work\data";
Directory.CreateDirectory(directory);
string path = Path.Combine(directory, "sample.txt");
7-3. UnauthorizedAccessException
UnauthorizedAccessExceptionは、アクセス権限がない場合や、フォルダをファイルとして開こうとした場合などに発生します。
たとえば、管理者権限が必要な場所に書き込もうとすると発生することがあります。
C#using FileStream fs = new FileStream(@"C:\Windows\sample.txt", FileMode.Create);
対策として、アプリケーションが書き込み可能なフォルダを使います。一般的には、ユーザーのドキュメントフォルダやアプリケーションデータフォルダなどを使うと安全です。
7-4. IOException
IOExceptionは、ファイル入出力全般のエラーで発生します。たとえば、ファイルが別プロセスでロックされている、ディスクに問題がある、読み書き中にエラーが起きた、などのケースです。
C#try
{
using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
}
catch (IOException ex)
{
Console.WriteLine($"I/Oエラー: {ex.Message}");
}
ファイルが使用中で開けない場合は、FileShareの指定を見直したり、再試行処理を入れたりします。
7-5. PathTooLongException
PathTooLongExceptionは、ファイルパスが長すぎる場合に発生します。深いフォルダ構造や長いファイル名を扱う場合に注意が必要です。
対策としては、保存先フォルダを浅くする、ファイル名を短くする、不要な階層を減らすなどがあります。
7-6. try-catchを使った安全な実装例
FileStreamを安全に使うには、usingとtry-catchを組み合わせます。
C#using System;
using System.IO;
using System.Text;
string path = "sample.txt";
try
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
string text = reader.ReadToEnd();
Console.WriteLine(text);
}
catch (FileNotFoundException)
{
Console.WriteLine("ファイルが見つかりません。");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("指定されたフォルダが存在しません。");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("ファイルにアクセスする権限がありません。");
}
catch (IOException ex)
{
Console.WriteLine($"ファイル入出力エラーが発生しました: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラーが発生しました: {ex.Message}");
}
例外を分けて処理すると、原因に応じたメッセージや対応を行いやすくなります。
7-7. ファイル存在チェックと権限チェックの注意点
File.Existsで存在チェックをしてから開くコードはよくあります。
C#if (File.Exists(path))
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
}
ただし、存在チェック後に別のプロセスがファイルを削除する可能性もあります。そのため、File.Existsだけに頼らず、実際のFileStream生成時の例外処理も必ず考慮しましょう。
権限チェックも同様です。事前に確認できても、実際に開く瞬間に権限やロック状態が変わる可能性があります。ファイル操作では「例外は起こり得るもの」と考えて実装することが大切です。
8. FileStreamの実践サンプルコード
8-1. ファイルを新規作成して書き込む
C#using System.IO;
using System.Text;
string path = "newfile.txt";
string text = "新しいファイルを作成して書き込みます。";
byte[] data = Encoding.UTF8.GetBytes(text);
using FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
fs.Write(data, 0, data.Length);
FileMode.CreateNewは、ファイルが既に存在すると例外になります。既存ファイルを誤って上書きしたくない場合に便利です。
8-2. 既存ファイルを読み込む
C#using System;
using System.IO;
using System.Text;
string path = "newfile.txt";
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
byte[] data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
string text = Encoding.UTF8.GetString(data);
Console.WriteLine(text);
既存ファイルを読む場合は、FileMode.OpenとFileAccess.Readを使います。
8-3. ファイルに追記する
C#using System.IO;
using System.Text;
string path = "log.txt";
string log = $"{DateTime.Now}: 処理を実行しました{Environment.NewLine}";
byte[] data = Encoding.UTF8.GetBytes(log);
using FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
fs.Write(data, 0, data.Length);
ログ出力のように末尾へ追加したい場合はFileMode.Appendを使います。
8-4. バイナリファイルをコピーする
C#using System.IO;
string sourcePath = "source.bin";
string destPath = "copy.bin";
byte[] buffer = new byte[81920];
using FileStream source = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using FileStream destination = new FileStream(destPath, FileMode.Create, FileAccess.Write, FileShare.None);
int bytesRead;
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0)
{
destination.Write(buffer, 0, bytesRead);
}
ポイントは、最後の書き込みでbuffer.LengthではなくbytesReadを指定することです。
8-5. CSVファイルを読み書きする
CSVを書き込む例です。
C#using System.IO;
using System.Text;
string path = "users.csv";
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);
writer.WriteLine("Id,Name,Age");
writer.WriteLine("1,山田太郎,30");
writer.WriteLine("2,佐藤花子,25");
CSVを読み込む例です。
C#using System;
using System.IO;
using System.Text;
string path = "users.csv";
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
string? line;
while ((line = reader.ReadLine()) != null)
{
string[] columns = line.Split(',');
Console.WriteLine(string.Join(" | ", columns));
}
実務では、値にカンマや改行、ダブルクォートが含まれるCSVもあるため、複雑なCSV処理には専用ライブラリの利用も検討しましょう。
8-6. 画像ファイルを読み込む
画像ファイルも、FileStreamではバイナリデータとして扱えます。
C#using System;
using System.IO;
string path = "image.png";
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
byte[] imageBytes = new byte[fs.Length];
fs.Read(imageBytes, 0, imageBytes.Length);
Console.WriteLine($"画像ファイルを {imageBytes.Length} バイト読み込みました。");
画像の中身を解析するには画像処理用ライブラリが必要ですが、ファイルとして読み込むだけならFileStreamで対応できます。
9. 非同期処理でFileStreamを使う方法
9-1. ReadAsyncとWriteAsyncの基本
FileStreamには、非同期読み書き用のReadAsyncとWriteAsyncがあります。これらを使うと、ファイルI/Oの待ち時間中にスレッドを占有しにくくなります。
C#int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
C#await fs.WriteAsync(buffer, 0, buffer.Length);
GUIアプリやWebアプリでは、I/O待ちで画面やリクエスト処理を止めないために非同期処理が有効です。
9-2. async/awaitを使ったサンプル
非同期でテキストファイルを読み込む例です。
C#using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "sample.txt";
using FileStream fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
useAsync: true
);
byte[] buffer = new byte[fs.Length];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
string text = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine(text);
}
}
非同期で書き込む例です。
C#using System.IO;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "async-output.txt";
byte[] data = Encoding.UTF8.GetBytes("非同期で書き込みます。");
using FileStream fs = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true
);
await fs.WriteAsync(data, 0, data.Length);
}
}
9-3. 同期処理と非同期処理の使い分け
小さなコンソールアプリで、短いファイルを1回だけ読み書きする程度なら、同期処理でも十分です。
一方で、次のような場合は非同期処理を検討します。
Webアプリでファイルをアップロード・ダウンロードする
GUIアプリで画面を固めたくない
大きなファイルを読み書きする
複数のファイルI/Oを並行して扱う
ネットワークドライブや外部ストレージを使う
非同期処理は便利ですが、コード全体がasync/await前提になるため、単純な処理では同期版のほうが読みやすいこともあります。
9-4. 大容量ファイル処理で非同期を使うメリット
大容量ファイルを扱う場合、ディスクI/Oの待ち時間が長くなることがあります。非同期処理を使うと、その待ち時間中に他の処理へ制御を戻しやすくなります。
たとえば、Webアプリで大きなファイルを保存する場合、同期I/Oでスレッドを長時間占有すると、同時アクセスに弱くなる可能性があります。非同期I/Oを使うことで、待機中のリソース効率を改善できます。
9-5. 非同期FileStream利用時の注意点
非同期処理を使うときは、次の点に注意しましょう。
ReadAsyncやWriteAsyncには必ずawaitを付けます。
C#await fs.WriteAsync(data, 0, data.Length);
awaitしないと、書き込み完了前に処理が進んでしまう可能性があります。
また、FileStreamを破棄するタイミングにも注意が必要です。非同期書き込みが終わる前にFileStreamが破棄されると、正しく書き込めません。
C#using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true);
await fs.WriteAsync(data, 0, data.Length);
awaitで完了を待ってからスコープを抜けるようにしましょう。
10. FileStreamでよくあるトラブルと解決方法
10-1. ファイルが使用中で開けない
「別のプロセスで使用されているため、プロセスはファイルにアクセスできません」というエラーはよくあります。
原因として多いのは、次のようなケースです。
自分のコードで
FileStreamを閉じ忘れている他のアプリがファイルを開いている
FileShare.Noneで排他的に開いているウイルス対策ソフトや同期ソフトが一時的に使用している
対策として、usingで確実に解放し、必要に応じてFileShare.Readなどを指定します。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
10-2. アクセス権限がなくて書き込めない
書き込み権限がない場所にファイルを作成しようとすると、UnauthorizedAccessExceptionが発生します。
対策は、書き込み可能な場所を使うことです。
C#string folder = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string path = Path.Combine(folder, "sample.txt");
アプリケーションの実行フォルダに書き込めるとは限らないため、ユーザーごとのデータフォルダを使う設計も検討しましょう。
10-3. 日本語が文字化けする
日本語が文字化けする原因は、書き込み時と読み込み時の文字コードが一致していないことが多いです。
書き込み時にUTF-8を指定したなら、読み込み時もUTF-8を指定します。
C#using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);
C#using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
古いシステムではShift_JISが使われている場合もあります。その場合は、対象ファイルの文字コードに合わせて読み込む必要があります。
10-4. ファイルパスが正しいのに見つからない
相対パスを使っている場合、思っているフォルダとは別の場所を基準にしていることがあります。
C#Console.WriteLine(Directory.GetCurrentDirectory());
このコードで実行時のカレントディレクトリを確認しましょう。
また、パスを手作業で連結している場合は、区切り文字のミスが起きることがあります。Path.Combineを使うと安全です。
C#string path = Path.Combine("data", "sample.txt");
10-5. 書き込んだ内容が反映されない
書き込んだ内容がすぐに見えない場合、バッファに残っている可能性があります。通常はusingで閉じれば反映されます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Create, FileAccess.Write);
fs.Write(data, 0, data.Length);
ファイルを開いたまま途中で内容を確認したい場合は、Flushを呼びます。
C#fs.Flush();
ただし、頻繁なFlushはパフォーマンス低下につながる場合があります。
10-6. ファイルサイズが大きいと処理が遅い
大きなファイルで処理が遅い場合は、次の点を見直します。
ファイル全体を一度に読み込んでいないか
小さすぎるバッファで何度も読み書きしていないか
不要な文字列変換を繰り返していないか
同期処理でUIやリクエスト処理を止めていないか
StreamReaderやStreamWriterを適切に使っているか
大容量ファイルでは、次のように分割処理するのが基本です。
C#byte[] buffer = new byte[81920];
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// bytesRead分だけ処理
}
11. FileStreamを使うときのベストプラクティス
11-1. usingで確実にリソースを解放する
FileStreamを使うときは、基本的にusingを使います。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
これにより、例外が発生してもリソースが解放されやすくなります。ファイルロックや閉じ忘れを防ぐためにも、usingは必須レベルで意識しましょう。
11-2. FileModeとFileAccessを目的に合わせて選ぶ
読み込みだけなら、次のように書きます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
書き込みだけなら、次のように書きます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Create, FileAccess.Write);
必要がないのにFileAccess.ReadWriteを使うと、意図しない変更やロック範囲の拡大につながることがあります。目的に合った最小限の権限を指定しましょう。
11-3. 文字列処理にはStreamReader・StreamWriterを使う
テキストファイルを扱う場合は、FileStreamだけで完結させるより、StreamReaderやStreamWriterを組み合わせるほうが読みやすくなります。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
string text = reader.ReadToEnd();
バイト処理はFileStream、文字列処理はStreamReader・StreamWriterと考えると整理しやすいです。
11-4. 大容量ファイルでは分割読み書きする
大きなファイルを一度に読み込むと、メモリ不足や処理速度低下の原因になります。
C#byte[] buffer = new byte[81920];
int read;
while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
{
destination.Write(buffer, 0, read);
}
ファイルコピー、アップロード、ダウンロード、ログ解析などでは、分割処理を前提に設計しましょう。
11-5. 例外処理を前提に実装する
ファイル操作では、ファイルが存在しない、権限がない、使用中で開けない、パスが不正など、さまざまな例外が起こります。
そのため、実務コードではtry-catchを使って、失敗時の動作を決めておくことが大切です。
C#try
{
using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
}
catch (IOException ex)
{
Console.WriteLine(ex.Message);
}
11-6. パフォーマンスと可読性のバランスを考える
FileStreamは細かく制御できる反面、コードが長くなりがちです。小さなテキストファイルを読むだけなら、次のようなコードで十分な場合もあります。
C#string text = File.ReadAllText("sample.txt");
一方、大容量ファイルやバイナリ処理ではFileStreamが向いています。常にFileStreamを使うのではなく、処理の目的に合わせて選びましょう。
12. FileStreamに関するよくある質問
12-1. FileStreamとFile.ReadAllTextはどちらを使うべき?
小さなテキストファイルを簡単に読みたいだけなら、File.ReadAllTextが便利です。
C#string text = File.ReadAllText("sample.txt");
一方で、大容量ファイル、バイナリファイル、細かい共有制御、非同期処理、分割読み込みが必要な場合はFileStreamが向いています。
12-2. FileStreamは必ずCloseする必要がある?
FileStreamは使い終わったら必ず閉じる必要があります。ただし、通常はCloseを直接呼ぶのではなく、usingで自動的にDisposeさせます。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open);
この書き方なら、スコープを抜けた時点で自動的にリソースが解放されます。
12-3. FileStreamで文字コードを指定できる?
FileStream自体はバイトを扱うクラスなので、文字コードを直接指定するクラスではありません。文字コードを指定したい場合は、Encodingを使って変換するか、StreamReaderやStreamWriterを使います。
C#using StreamReader reader = new StreamReader(fs, Encoding.UTF8);
C#using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);
12-4. FileStreamで追記するにはどう書く?
追記するには、FileMode.Appendを使います。
C#using System.IO;
using System.Text;
string path = "log.txt";
byte[] data = Encoding.UTF8.GetBytes("追記するテキスト\n");
using FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write);
fs.Write(data, 0, data.Length);
テキストを追記するならStreamWriterも便利です。
C#using FileStream fs = new FileStream("log.txt", FileMode.Append, FileAccess.Write);
using StreamWriter writer = new StreamWriter(fs, Encoding.UTF8);
writer.WriteLine("追記するテキスト");
12-5. FileStreamは大容量ファイル処理に向いている?
はい、FileStreamは大容量ファイル処理に向いています。ファイル全体を一度にメモリへ読み込まず、バッファを使って少しずつ読み書きできるためです。
C#byte[] buffer = new byte[81920];
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
// 分割して処理
}
ただし、処理内容によってはBufferedStream、非同期I/O、専用ライブラリなども検討するとよいでしょう。
12-6. FileStreamでファイルロックを避ける方法は?
ファイルロックを避けるには、まずusingで確実に閉じることが重要です。
C#using FileStream fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read);
また、他のプロセスにも読み取りを許可したい場合はFileShare.Readを指定します。
C#using FileStream fs = new FileStream(
"sample.txt",
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
ログファイルなど、他の処理が読み取る可能性のあるファイルでは、FileShareの指定を適切に行いましょう。
まとめ
C#のFileStreamは、ファイルをバイト単位で読み書きするための基本クラスです。単純なテキスト操作だけならFile.ReadAllTextやFile.WriteAllTextでも十分ですが、大容量ファイル、バイナリファイル、追記処理、共有制御、非同期処理などではFileStreamが役立ちます。
初心者がまず押さえるべきポイントは、次のとおりです。
FileStreamはファイルをバイト単位で扱うテキスト処理では
StreamReaderやStreamWriterを組み合わせるFileModeで作成・上書き・追記・オープン方法を指定するFileAccessで読み取り・書き込み権限を指定するFileShareで他プロセスとの共有方法を指定するusingで必ずリソースを解放するファイル操作では例外処理を前提にする
大容量ファイルでは分割読み書きを行う
非同期処理では
ReadAsyncやWriteAsyncを使う
csharp filestreamを理解するうえで大切なのは、いきなり難しい実装に進むのではなく、まず「開く」「読む」「書く」「閉じる」の流れを確実に押さえることです。そのうえで、FileMode、FileAccess、FileShare、using、例外処理を組み合わせれば、安全で実用的なファイル読み書き処理を書けるようになります。

