手続き型音楽の日常

関数型音楽に乗り換えたい

お兄ちゃん!そこは MemoryStream の出番だよ!

タイトルは釣りです(お約束)

MemoryStream のススメ

みなさん、 System.IO.MemoryStream 使っていますか。私はよく使いますよ。

MemoryStream Class (System.IO) | Microsoft Docs

リアクティブプログラミングだったり、Java の Stream API だったり、いろんな Stream がありますが、今回はC#MemoryStream に注目してみます。

MemoryStream のイロハ

そもそもC#には Stream クラスがあり、 MemoryStream はその派生クラスです。同じような派生クラスには FileStreamCryptoStream があります。

似て非なるものですが、これらは共通して データを順次読み出したり、順次格納したりできる という特徴を持っています。

たとえば 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 に暗号化されたデータが書き込まれる(暗号化されたデータがファイルに書き込まれる) コードです。

驚くべきことに、このコードを先ほどのコードと比べると、 MemoryStreamFileStream にすり替えただけなのです。

MemoryStream は世界を救う

つまり、 MemoryStream は、 byte[]FileStream 、すなわち 変数操作とファイル操作と同等に扱えるようにするクラス ということなのです。

C# では、とくにデータの変換系の処理を Stream で行うような風潮があるように見えます。

例えば暗号化や、巨大なバイナリファイルの符号化など。JSONシリアライズにも Stream クラスを引数に受けるメソッドを用います。

こうすることで、対象がファイルでもメモリでも、同じ操作でデータを扱えるという素晴らしい恩恵を享受することができます。

単体テストもコードの再利用もどんとこい、な機能ですね。

さいごに

素人が生意気にすみませんでした 。ちょっと魔がさして書き始めたら収拾がつかなくなってしまいました。申し訳ありません。反省はしていません。

この間 JSONC#のクラスにシリアライズしたりデシリアライズするときに MemoryStream を使う機会があったのですが、正直な話

なんでクラスで管理できる情報量をわざわざ MemoryStream で書く必要があるのか、変数でいいのではないか

と思いながらバリバリ書いてたんですね。

そしてふと、思いついたんですよね。JSON ってファイルの可能性があるよなぁ、と。

自分の中ではとても面白い発見だったので、ついつい長ったらしく書いてしまいました。本当に申し訳ありませんでした。

最後まで見てくださって本当にありがとうございました。

P.S.

本来書きたかったネタもとりあえず置いておきます。 MemoryStreamRead メソッドが超絶使いにくい件について。

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;
    }
}

現在 0000/00/00 00:00 を生きています。