C++だとこんな感じで簡単にint配列をbyte(char)配列として取り扱うことができます。
int* intArray = new int[2]; intArray[0] = -1; //32ビット全部1が立つ intArray[1] = 2; for(int i = 0; i < 2; i++) { cout << intArray[i] << endl; } cout << "======" << endl; char* ptr = (char*)intArray; for(int i = 0; i < 8; i++) { cout<< +ptr[i] << endl; }
出力は
-1 2 ====== -1 -1 -1 -1 2 0 0 0
ではC#はどうすればいいか。 C#ではC++っぽいキャストは不可能です。 素朴には、以下みたいに新しく配列を用意してコピー、みたいなことをすればいいわけです。
int[] intArray = new int[2]; intArray[0] = -1; intArray[1] = 2; var byteArray = new byte[8] Buffer.BlockCopy(intArray, 0, byteArray , 0, 8);
が、コピーなんざしたくないわけです。目の前にある配列を違う型として読みたいだけなので...。
で、結論から言えばMemoryMarshal.Cast
とかMemoryMarshal.AsBytes
を使えばよさそうです。T[ ]みたいな配列ではなくSpan<T>が返ってくるわけですが、特に問題はない。
( @ikorin24に感謝。)
MemoryMarshalに生えてる関数たちめっちゃ便利...!
これを叩くことでコピーなしに、int [ ] をbyte[ ] (正確にはSpan<byte>)として簡単に扱えるようになります。
var intArray = new int[2]; intArray[0] = -1; Span<byte> byteSpan = MemoryMarshal.Cast<int, byte>(intArray.AsSpan()); Console.WriteLine($"intArray.Length is {intArray.Length}. byteSpan.Length is {byteSpan.Length}."); foreach (var b in byteSpan) { Console.WriteLine(b); }
出力は
intArray.Length is 2. byteSpan.Length is 8. 255 255 255 255 0 0 0 0
と、なります。ちゃんとLength も適切な値になってますね、よしよし。
まとめ
- MemoryMarshal.Castとかすればコピーせずにint[ ]をbyte列(Span<byte>)として扱える!
- MemoryMarshalにはSpan, Memoryがらみの便利関数がたくさん生えてるので認知しておこう。
余談
ここから余談です。実際MemoryMarshal.Cast
やMemoryMarshal.AsBytes
を使えばいいので、マジで余談です。
さて、冒頭に書いたようにC#で簡単にint[ ]からbyte[ ]とかに変換したいんですが、それはできません、というのもC++と違って配列分のメモリ確保してその先頭アドレスを返しているだけでは当然ないからです。 でないと、.Lengthとかで配列長なんて取得できません。 C#でなんかオブジェクト作った場合、オブジェクトの先頭に型情報とかが入ってるヘッダがくっついてます。 で、配列の場合にはヘッダのあとに配列長(64bit環境だったら64bit、たぶん)が、その後に配列用に確保されたメモリが続いてる感じ。
| ヘッダー | 配列長 | [0] | [1] | [2] | .....
ufcppさんところの画像が分かりやすい。
なので、Unsafe.Asとか強制的に型を変換したところで配列長とかがよしなに制御されるわけではないので、こんな感じになります。
int[] intArray = new int[5]; var byteArray = Unsafe.As<int[], byte[]>(ref intArray); Console.WriteLine($"intArray.Length is {intArray.Length}. byteArray.Length is {byteArray.Length}."); // 出力 // intArray.Length is 5. byteArray.Length is 5.
intが4バイトなんで、byte[]にしたとき20バイトになってほしいのになってくれない。当たり前ですね。 さて、C#ではポインタが使えますし、配列長保存しているところ書き換えたくね?とMemoryMarshalを知らなかった私はなりました。無知は怖いですね!
ヘッダとか長さはランタイムとかのための情報なので、無理やりポインタで弄ったらGCがどんな風に動くかも全くわからなし危険極まりないですが、Rewriteしていきます。 (ところで2021年に入ってRewriteが10年前のゲームに仲間入りしました。時が恐ろしい)
こんな感じ。
int[] intArray = new int[5]; intArray[0] = 1; intArray[1] = 2; intArray[2] = 4; intArray[4] = -1; Console.WriteLine($"intArray.Length is {intArray.Length}."); var byteArray = Cast(intArray); Console.WriteLine($"intArray.Length is {intArray.Length}. byteArray.Length is {byteArray.Length}."); foreach (var b in byteArray) { Console.WriteLine(b); } private static unsafe byte[] Cast(int[] array) { byte[] byteArray = Unsafe.As<int[], byte[]>(ref array); fixed (byte* ptr = &byteArray[0]) { long* lengthHeader = (long*)(ptr - 8); *lengthHeader = array.Length * sizeof(int); } return byteArray; }
出力が
intArray.Length is 5. intArray.Length is 20. byteArray.Length is 20. 1 0 0 0 2 0 0 0 4 0 0 0 0 0 0 0 255 255 255 255
ちなみに*lengthHeader = -1
とかすると.Lengthが-1を返す配列を作ることができてしまいます。
やばさがプンプン漂っています。
この無理やり長さの部分をいじくったやつをMemoryMarshal.Castとかしてみましょう。
int[] intArray = new int[5]; intArray[0] = 1; intArray[1] = 2; intArray[2] = 4; intArray[4] = -1; Console.WriteLine($"intArray.Length is {intArray.Length}."); var byteArray = Cast(intArray); Console.WriteLine($"intArray.Length is {intArray.Length}. byteArray.Length is {byteArray.Length}."); foreach (var b in byteArray) { Console.WriteLine(b); } Console.WriteLine("======"); var span = MemoryMarshal.Cast<int, byte>(intArray.AsSpan()); Console.WriteLine($"span.Length is {span.Length}."); foreach (var s in span) { Console.WriteLine(s); }
出力がこう。
intArray.Length is 5. intArray.Length is 20. byteArray.Length is 20. 1 0 0 0 2 0 0 0 4 0 0 0 0 0 0 0 255 255 255 255 ====== span.Length is 80. 1 0 0 0 2 0 0 0 4 0 0 0 0 0 0 0 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0 0 240 177 190 231 249 127 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 24 102 190 231 249 127 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
キャッキャッ。確保してないメモリが見えてしまうのC++みがある。
はい、以上余談でした。黙ってMemoryMarshalのお世話になりましょう。