お兄ちゃん!そこは MemoryStream の出番だよ!
タイトルは釣りです(お約束)
MemoryStream のススメ
みなさん、 System.IO.MemoryStream
使っていますか。私はよく使いますよ。
MemoryStream Class (System.IO) | Microsoft Docs
リアクティブプログラミングだったり、Java の Stream API だったり、いろんな Stream がありますが、今回はC#の MemoryStream
に注目してみます。
MemoryStream のイロハ
そもそもC#には Stream
クラスがあり、 MemoryStream
はその派生クラスです。同じような派生クラスには FileStream
や CryptoStream
があります。
似て非なるものですが、これらは共通して データを順次読み出したり、順次格納したりできる という特徴を持っています。
たとえば FileStream
は、ディスク上のファイルを読み書きするクラスです。ファイルを開くと FileStream
にはそのファイルサイズと カーソル位置 が保持されます。
カーソル位置は Stream
のデータを操作する位置 を表します。これを使うと、例えばファイルの80バイト目から60バイト分読み出したい、といった場合に、カーソル位置を80バイト目に移動させ、そこから60バイト分読み出すことができます(同時にカーソルも60バイト動くため、次に60バイトを読み出すと140バイト目から60バイトを読み出します)。
これは、次のように書くことができます。
// ファイル名 を元に FileStream を作成 var stream = new FileStream("C:/hoge.txt", FileMode.Open); // 80バイト目から60バイト読み出す var puts = new byte[60]; stream.Position = 80; stream.Read(puts, 0, 60);
同様に 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);
なお、以下の配列へのアクセスをするコードで、同様の 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];
え、じゃあ何に使うん 。
MemoryStream は byte[]
へのアクセスを簡単にします。本当に?
ひどい夢を見ました。すべての byte[]
配列へのアクセスを、 MemoryStream
を通じて行うようにするという、お達しが出たのです。まったく、とんだ災難です。このプロダクトは文字列や数値としては扱えないデータが山ほどあり(画像や音声、もしかしたら地球外生命体の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);
AesManaged Class (System.Security.Cryptography) | Microsoft Docs
CryptoStream Class (System.Security.Cryptography) | Microsoft Docs
ちょっと難解ですが、 ある 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 は世界を救う
つまり、 MemoryStream
は、 byte[]
を FileStream
、すなわち 変数操作とファイル操作と同等に扱えるようにするクラス ということなのです。
C# では、とくにデータの変換系の処理を Stream
で行うような風潮があるように見えます。
例えば暗号化や、巨大なバイナリファイルの符号化など。JSONのシリアライズにも Stream
クラスを引数に受けるメソッドを用います。
こうすることで、対象がファイルでもメモリでも、同じ操作でデータを扱えるという素晴らしい恩恵を享受することができます。
単体テストもコードの再利用もどんとこい、な機能ですね。
さいごに
素人が生意気にすみませんでした 。ちょっと魔がさして書き始めたら収拾がつかなくなってしまいました。申し訳ありません。反省はしていません。
この間 JSON をC#のクラスにシリアライズしたりデシリアライズするときに MemoryStream
を使う機会があったのですが、正直な話
なんでクラスで管理できる情報量をわざわざ MemoryStream
で書く必要があるのか、変数でいいのではないか
と思いながらバリバリ書いてたんですね。
そしてふと、思いついたんですよね。JSON ってファイルの可能性があるよなぁ、と。
自分の中ではとても面白い発見だったので、ついつい長ったらしく書いてしまいました。本当に申し訳ありませんでした。
最後まで見てくださって本当にありがとうございました。
P.S.
本来書きたかったネタもとりあえず置いておきます。 MemoryStream
の Read
メソッドが超絶使いにくい件について。
public static class MemoryStreamExtention { public static byte[] ReadAllBytes(this System.IO.MemoryStream target) { byte[] ret = new byte[target.Length]; target.Position = 0; target.Read(ret, 0, (int)target.Length); return ret; } }