はじめに
C#のStreamクラスがなんのことかさっぱりわからなかったので纏める。
なぜStream継承クラス(FileStream/MemoryStream...)でデータを管理する必要があるのか?変数でいいのでは?と思った。さらにStreamと書いてあると、抽象的でよくわからない…。
ストリームとは
プログラミングの分野では、データの入出力全般を扱う抽象的なオブジェクトやデータ型を意味する場合が多い。データが出入りする何らかの対象(メモリ領域やファイル、ネットワークなど)をプログラム中で扱えるように抽象化したもので、接続や切断、書き込みや読み込みなどを簡易な操作で行うことができる。
FileStreamクラス
FileStream.Read()で読み込まれるbyte[]はファイルのエンコーディング方式に従ってバイナリデータに変換される。ファイルがUTF-8で保存されていれば、UTF-8でエンコードされる。
MemoryStreamクラス
MemoryStreamを使って、バイト配列を読み込んだ時。
MemoryStream も、ある byte[] のサイズ(配列の長さ)とカーソル位置が保持されます。
そして、例えばある byte[] の80バイト目から60バイト分読み出したい、といった場合に、カーソル位置を80バイト目に移動させ、そこから60バイト分読み出すことができます。
これは、次のように書くことができます。var array = new byte[300]; // --- ここに本来はデータの操作が入る --- // // array を元に MemoryStream を作成 var stream = new MemoryStream(array); // 80バイト目から60バイト読み出す var puts = new byte[60]; stream.Position = 80; stream.Read(puts, 0, 60);
使用せずに読み込んだ時。MemoryStream使う意味あるの?
なお、以下の配列へのアクセスをするコードで、同様の puts を得られます。
var array = new byte[300]; // --- ここに本来はデータの操作が入る --- // // 80バイト目から60バイト読み出す var puts = new byte[60]; for (int i = 0; i < 60; i++) puts[i] = array[80 + i];
え、じゃあ何に使うん 。
使う意味あるらしい。
このプロダクトは文字列や数値としては扱えないデータが山ほどあり(画像や音声、もしかしたら地球外生命体のDNAの解析結果かもしれない)、それらはすべて byte[] で表すことになっています。だから、それら全てのアクセスを、 MemoryStream に置き換えなければなりません。たった1要素の読み込みでさえ、長ったらしく2,3行を書き連ねなければならないのです。
実際にそんなことがあるはずはありません。安心してください。 ところで次の例を見てくれ、こいつをどう思う?
// AESで暗号化するためのオブジェクトを初期化 var aes = new AesManaged(); aes.GenerateIV(); aes.GenerateKey(); // MemoryStreamを作成 var memStream = new MemoryStream(); // CryptoStreamを作成 var cryStream = new CryptoStream(memStream, aes.CreateEncryptor, >CryptoStreamMode.Write); // CryptoStreamに書き込み cryStream.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5); // MemoryStreamから読み出し var read = byte[3]; memStream.Position = 1; memStream.Read(read, 0, 3);
ちょっと難解ですが、 ある MemoryStream を参照する CryptoStream にデータを書き込むと、 MemoryStream に暗号化されたデータが書き込まれる コードです。
上記の例では read に暗号化されたデータの一部が代入されることになります。
さてこれをちょっとだけ改変しましょう。
// AESで暗号化するためのオブジェクトを初期化 var aes = new AesManaged(); aes.GenerateIV(); aes.GenerateKey(); // FileStreamを作成 var filStream = new FileStream("C:/hoge.enc", FileMode.Create); // CryptoStreamを作成 var cryStream = new CryptoStream(filStream , aes.CreateEncryptor, CryptoStreamMode.Write); // CryptoStreamに書き込み cryStream.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5);
だいたい想像がつくと思いますが、これは ある FileStream を参照する CryptoStream にデータを書き込むと、 FileStream に暗号化されたデータが書き込まれる(暗号化されたデータがファイルに書き込まれる) コードです。
驚くべきことに、このコードを先ほどのコードと比べると、 MemoryStream を FileStream にすり替えただけなのです。
つまり、 MemoryStream は、 byte[] を FileStream 、すなわち 変数操作とファイル操作と同等に扱えるようにするクラス ということなのです。
C# では、とくにデータの変換系の処理を Stream で行うような風潮があるように見えます。
バイト配列に読み出す。モックや一時的なバッファとして使われることが多い。
結論
文字列や数値(変数)では扱えない巨大なデータ(画像や音声等)をbyte配列を持ったストリームで管理するためのクラスと思われる。
(2025/2/17追記)基本的にはStreamにすべて読み出すのではなく、必要な時に必要なデータだけ読みこみ、メモリを節約する。Streamの本質的な強みは、逐次処理(シーケンシャルな読み書き)を可能にすることと、ほかのストリームと組み合わせてデータ変換を行うことにある。
Stream と IEnumerable / IAsyncEnumerable の類似性
Streamでは必要な時に必要なデータを読み込む使い方が一般的。
IEnumerable / IAsyncEnumerableも同じことが可能。
① 例:List での逐次処理
ListはIEnumerableを実装しているため、foreachを使って1 つずつ処理できる。
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
foreach (var name in names)
{
Console.WriteLine(name); // "Alice" → "Bob" → "Charlie" の順に処理
}
この場合、namesに含まれる各要素(string 型のオブジェクト)を1つずつ逐次的に処理している。
ただし、Listの場合はすべてのデータがメモリ上にあるため、データを遅延取得するわけではないことに注意すること。
② IEnumerable による遅延評価の逐次処理
IEnumerableはデータをすべてメモリに保持しなくても、逐次的に1つずつ処理することができる仕組みを持っている。
例えば、yield returnを使うと、リスト全体をメモリに持たずに、1つずつデータを返すようになる。
IEnumerable<int> GenerateNumbers()
{
for (int i = 1; i <= 5; i++)
{
yield return i; // 1 つずつデータを返す
}
}
foreach (var num in GenerateNumbers())
{
Console.WriteLine(num); // 1 → 2 → 3 → 4 → 5 の順に処理
}
この場合、リスト全体を作らずに、1つずつデータを生成しながら処理している。