1. はじめに
ちょっとケトル(やかん)を想像してみて。蛇口をひねると水が流れ出すよね。いっぱい水をためて一気に注ぐこともできるし、ちょっとずつケトルに入れることもできる。ファイルも同じで、全部を一気にメモリに読み込むのが便利な時もあれば、そうじゃない時もある。ファイルがめっちゃ大きいこともあるし、データソースがファイルじゃなくてネットワーク接続みたいに、データが少しずつ届く場合もあるんだ。
もし常にバイト配列だけでやり取りしようとしたら、大きなファイルだとすぐメモリが足りなくなるし、「無限」データストリーム(たとえば動画や音声ストリーム)だとこのやり方は全然ダメ。そこでストリームのコンセプトが役立つってわけ!
.NETのストリームは、データに順番にアクセスするための抽象化されたもの。データソースがファイルでもネットワークでもメモリでも、あるいは圧縮アーカイブみたいなちょっと変わったものでもOK。ストリームを使えば、データを部分的に(たいていはブロックやバイト単位で)読み書きできるんだ。
基本アイデア:
- ストリームはデータをやり取りするためのチャンネルみたいなもの。コンベアみたいに「置く」(書き込む)か「取る」(読む)だけで、中身がどこにどう保存されてるかは気にしなくていい。
- データは順番にやってくる:前の部分を読んでからじゃないと次の部分は読めない(逆もできる場合はシーク対応)。
- たいていの場合、全部のデータを一気にメモリに持たない(パソコンもそれを喜ぶよ)。
この抽象化は.NETのほぼすべての入出力操作の基礎になってる:ファイル操作、ネットワーク、アーカイブ、コンソール操作まで!
2. ストリーム System.IO.Stream
継承とアーキテクチャ: System.IO.Stream
.NETのほぼすべてのストリームは抽象クラスSystem.IO.Streamを継承してる。これが、読み書きやシーク、ストリームの管理のための基本メソッドを定義してるんだ。
classDiagram
class Stream {
+Read()
+Write()
+Seek()
+CanRead
+CanWrite
+CanSeek
+Length
+Position
}
class FileStream
class MemoryStream
class NetworkStream
class CryptoStream
Stream <|-- FileStream
Stream <|-- MemoryStream
Stream <|-- NetworkStream
Stream <|-- CryptoStream
- Stream — 基本の抽象クラス
- FileStream — ファイル操作用
- MemoryStream — メモリ上のデータ操作用
- NetworkStream — ネットワーク通信用
- CryptoStream — 暗号化/復号化用
ストリームの主なプロパティとメソッドざっくり紹介
| プロパティ / メソッド | 説明 |
|---|---|
|
このストリームから読み込みできるかどうか |
|
このストリームに書き込みできるかどうか |
|
ストリーム内で位置移動できるか(全部が対応してるわけじゃない) |
|
ストリームの長さ(対応してる場合のみ) |
|
ストリーム内の現在位置 |
|
データの読み込み |
|
データの書き込み |
|
ストリーム内の位置移動 |
|
バッファのフラッシュ(たまったデータを一気にストリームへ書き出す) |
/ |
ストリームを閉じてリソースを解放 |
じゃあ、実際にどう使うのか見てみよう。
3. 例: Streamでファイルを読み書きしてみる
ストリームが「実際どう動くか」っていう最小限の例を紹介するね:
// 書き込み用にファイルを開く
using var stream = new FileStream("numbers.bin", FileMode.Create);
// 1から10までの数字をファイルに書き込むとする
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // 1バイトずつ書き込む
}
// 明示的にファイルを閉じて、今度は読み込み用に開く
stream.Close();
// じゃあ今度はその数字を読み込んでみよう
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // 1, 2, ... 10 が出力される
}
ここではFileStreamを使ってるけど、これはまさに「ストリーム」そのもの。データをブロックやバイト単位で読み書きできるよ。
ストリームの種類:どこで使われてる?
ストリームって、別にディスク上のファイルだけじゃない。こんな例があるよ:
- ディスク上のファイル(たとえばFileStream — 一番よくあるやつ)
- メモリ上のストリーム(MemoryStream — 一時的なデータや中間データに便利)
- ネットワーク接続(NetworkStream)
- 圧縮/アーカイブ(GZipStream, DeflateStream)
- 暗号化(CryptoStream)
- コンソール入出力(そう、これも!)— 技術的にはこれもストリーム
これのおかげで、データの出どころや行き先を気にせずコードを書ける。ストリームでやり取りできれば、どこでも動く汎用的なコードになる!
4. ちょっとしたコツ
読み書きは「部分的に」データをやり取りする操作。たいていはバイト配列とRead、Writeメソッドを使うよ。
例:ファイルをブロックごとに読む
byte[] buffer = new byte[1024]; // 1024バイト(1KB)のバッファ
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// bufferの中のbytesReadバイトだけ処理する
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"ブロックの合計: {sum}");
}
このやり方はウイルス対策ソフトから音楽プレイヤーまで、いろんなところで使われてる。
ストリーム内の位置指定(Position, Seek)
多くのストリーム(たとえばファイル系)では、データの「次」だけじゃなくて、好きな位置にジャンプしてそこから読み書きできる。
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // 6バイト目に移動(0から数える)
int value = stream.ReadByte();
Console.WriteLine($"ファイルの6バイト目: {value}");
ストリームには「読み込み専用」「書き込み専用」「両方対応」がある
ストリームによっては、どっちかしかできない場合もある:
- 書き込み専用で開いたファイル:Write()だけ
- ネットワークデータの読み込み用ストリーム:Read()だけ
- ちょっと変わったケース(たとえばプリンタ出力用ストリーム)だと「巻き戻し」や位置移動(シーク)ができないこともある。
どんな操作ができるかは、CanRead、CanWrite、CanSeekプロパティでチェックしよう:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("読み込み対応");
if (stream.CanWrite)
Console.WriteLine("書き込み対応");
if (stream.CanSeek)
Console.WriteLine("ファイル内の位置移動OK");
ストリームのバッファリング
ほとんどのストリームはパフォーマンス向上のために内部バッファを使ってる。バッファリングでディスクやネットワークへのアクセス回数を減らせる:データを内部でためてからまとめてやり取りする感じ。
Flush()メソッドでバッファをフラッシュできる(たとえば、絶対にディスクに書き込みたいときとか):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // これで確実にディスクに書き込まれる
もし大事なデータ(たとえば決済トランザクション!)を書き込むなら、Flush()は必須だよ。
5. ストリーム操作でありがちなミス
初心者がよくやるミスはこんな感じ:
ストリームを閉じ忘れる(メモリリークや「ロックされた」ファイルなど、いろいろ面倒なことになる)。
テキストストリームとバイナリストリームを混同して、バイトメソッドで文字列を書き込んで「文字化け」する。
バッファが小さすぎる(またはバッファなし)で、処理がめっちゃ遅くなる。
Read()が必ず指定したバイト数だけ読むと思い込む—実はそれより少ない場合もあるので、戻り値は必ずチェックしよう。
全部のストリームが位置移動(Seek)に対応してると思い込む(特にネットワーク系は非対応が多い)。
たとえばこんな感じ:
// ダメな例:実際に読んだバイト数をチェックせずに全部読むつもりでいる
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// ファイルが小さい場合、bytesReadは1024より小さいかも!
}
GO TO FULL VERSION