.NET Frameworkにおいてバッファなどのバイト列を扱うクラスにはMemoryStreamが存在しますが、Bufferクラスのメソッドを用いることによってプリミティブ型の配列を直接バイト列として操作することができます。 例えば、int
型の配列に対してバイト単位で値の取得・設定をしたり、memcpy
のように配列の一部をバイト列としてコピーすることができます。
こういった操作はシフト演算やポインタを介して行うこともできますが、Bufferクラスを用いればポインタ、つまりunsafe
コンテキストによるアンセーフコードを一切用いずに記述することができます。
Bufferクラス
Bufferクラスは配列をバッファとして扱い、バイト単位での操作を行うためのメソッドを持ったクラスです。 例えば、int
型配列に対して、1バイト単位で読み取り・書き込みをしたり、memcpy
・memcpy_s
のようにメモリブロックのコピーを行うためのメソッドなどが提供されています。
Bufferクラスは、リングバッファなどのバッファ機能を実装するクラスではありません。 バッファとなるメモリ領域を確保し、確保した領域に読み書きする目的にはSafeBufferクラス、ストリームへの読み書きに対してバッファリングを行う目的にはBufferedStreamクラスなど、バッファ機能を扱うクラスは別に存在します。
Bufferクラスで扱える配列型
Bufferクラスはプリミティブ型の配列に対してのみ用いることができます。 その他の型の配列、構造体やクラスの配列に対しては用いることができません。 .NET Frameworkではbyte
, int
, char
, double
, IntPtrなどの型がプリミティブ型として扱われます。 ここで言うプリミティブ型とは言語の組み込み型とは異なります。 例えばdecimal
やint?
は組み込み型ですがプリミティブ型ではありません。
数値型 | 整数型 |
sbyte , short , int , long byte , ushort , uint , ulong |
---|---|---|
実数型 |
single , double |
|
ポインタ型 |
IntPtr , UIntPtr |
|
文字型 |
char
|
|
ブール型 |
bool
|
プリミティブ型についてや型の分類等について詳しくは型の種類・サイズ・精度・値域を参照してください。
配列のバイト列操作
バイト単位での値の設定・取得 (GetByteメソッド・SetByteメソッド)
Buffer.GetByteメソッドおよびBuffer.SetByteメソッドを用いると、プリミティブ型配列に対してバイト単位での値の取得・設定を行うことができます。 これは、任意のプリミティブ型配列をバイト配列と同様に扱うことができることを意味します。
例えば配列int[]
をポインタbyte*
にキャストしてバイト列として扱うような操作は、このメソッドによって代用することができます。 したがって、GetByteメソッド・SetByteメソッドを用いれば、unsafe
なコードの記述を避けることができます。
上記の例において、配列内の各値はリトルエンディアンで格納されている点に注意してください。 配列arrのバイト表現を図式化すると次にようになります。
オフセット(バイト) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
要素値 | 0x00010203 | 0x04050607 | 0x08090A0B | 0x0C0D0E0F | ||||||||||||
バイト値 | 0x03 | 0x02 | 0x01 | 0x00 | 0x07 | 0x06 | 0x05 | 0x04 | 0x0B | 0x0A | 0x09 | 0x08 | 0x0F | 0x0E | 0x0D | 0x0C |
配列のバイト数の取得 (ByteLengthメソッド)
GetByteメソッド・SetByteメソッドに加えて、Buffer.ByteLengthメソッドを使うと配列のバイト数を取得することができます。 配列のバイト数は、sizeof
演算子で型のサイズを求め、配列の長さとの積をとることでも求めることができますが、このメソッドでは配列の型が何であるかを意識しなくてもメソッド呼び出しのみで配列のバイト数を求めることができます。
GetByteメソッド・SetByteメソッドと同様、ByteLengthメソッドもプリミティブ型配列に対してのみ用いることができます。
C#におけるsizeof
演算子については構造体のサイズ §.sizeof演算子 (C#)を参照してください。
Bufferクラスによるバイト列操作の例
以下ではGetByteメソッド・SetByteメソッドを使った例を掲載します。 いずれもパフォーマンス上の観点からはポインタ演算やシフト演算を用いた方がよいものですが、GetByteメソッド・SetByteメソッドを用いることでunsafe
コンテキストを使った記述を排除できる=マネージドコードのみを用いても記述できることを示しています。
バイト列のコピー
Bufferクラスでは、配列に対してmemcpy
やmemmove
に相当する操作を行うためのBuffer.BlockCopy・Buffer.MemoryCopyといったメソッドが用意されています。 これらのメソッドを使うと、配列を単なるバイト列やメモリブロックとして扱い、配列の型によらずバイト単位でのコピーを行うことができます。
BlockCopyメソッド
Buffer.BlockCopyメソッドは、配列をバイト列とみなしてその内容を別の配列へとコピーします。 memcpy
やmemmove
に相当する操作は、このBlockCopyメソッドか、後述のMemoryCopyメソッドを使うことで実現できます。
配列をコピーするという点ではBuffer.BlockCopyメソッドとArray.Copyメソッドは似ていますが、BlockCopyメソッドは要素単位ではなくバイト単位でのコピーを行います。 引数の指定順序も同じですが、各引数の意味は以下のように異なるため、Array.CopyメソッドとBuffer.BlockCopyメソッドを単純に置き換えて使うことはできません。
メソッド呼び出し | メソッドの動作 |
---|---|
Buffer.BlockCopy(source, 0, dest, 2, 6)
|
配列sourceのオフセット0から6バイト分を配列destのオフセット2以降にコピーする |
Array.Copy(source, 0, dest, 2, 6)
|
配列sourceのインデックス0から6要素分を配列destのインデックス2以降にコピーする |
上記の例において、配列内の各値はリトルエンディアンで格納されている点に注意してください。 配列sourceとdestの内容を図式化すると次にようになります。
オフセット(バイト) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
要素値 | 0x00010203 | 0x04050607 | 0x08090A0B | 0x0C0D0E0F | ||||||||||||
バイト値 | 0x03 | 0x02 | 0x01 | 0x00 | 0x07 | 0x06 | 0x05 | 0x04 | 0x0B | 0x0A | 0x09 | 0x08 | 0x0F | 0x0E | 0x0D | 0x0C |
オフセット(バイト) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
要素値 | 0x0203FFFF | 0x06070001 | 0xFFFFFFFF | 0xFFFFFFFF | ||||||||||||
バイト値 | 0xFF | 0xFF | 0x03 | 0x02 | 0x01 | 0x00 | 0x07 | 0x06 | 0xFF | 0xFF | 0xFF | 0xFF | 0xFF | 0xFF | 0xFF | 0xFF |
このように、BlockCopyメソッドではコピー元・コピー先をバイト単位で指定することができるため、要素をまたがる位置からの/へのコピーをすることもできます。
さらにBlockCopyメソッドでは、コピー先がプリミティブ型配列であれば異なる型の配列へのコピーをすることもできます。 そのため、BlockCopyメソッドを使うことによって配列のバイト単位での内容を維持したまま別の型の配列へと変換することができます。
この例のように、BlockCopyメソッドではuint
→byte
のように型としては互換性のない変換(縮小変換となる変換)でもコピーを行うことができます。 Array.Copyメソッドでは、uint[]
→byte[]
へのコピーのように縮小変換を伴うコピーを行おうとすると例外ArrayTypeMismatchExceptionがスローされます。
型の変換の種類、縮小変換については基本型の型変換を参照してください。
BlockCopyメソッドでは、コピー元とコピー先の領域がオーバーラップしていても(重なった状態でも)、適切にコピーが行われます。 オーバーラップした部分のコピー中の状態がコピー先に反映されることはありません。 (この動作は、Array.Copyメソッドでも同様です)
Buffer.BlockCopyメソッドとArray.Copyメソッドの相違点を整理すると次のようになります。
操作 | Buffer.BlockCopy | Array.Copy |
---|---|---|
コピーの単位 | バイト単位 | 要素単位 |
要素をまたがる位置からの/へのコピー | 可 | 不可 |
異なる型の配列へのコピー | プリミティブ型であれば可 (byte ⇄int ,int ⇄double など)それ以外の型の場合はArgumentException |
プリミティブ型かつ型に互換性がある場合のみ可 (byte →int ,int →double など)それ以外の型の場合はArrayTypeMismatchException (詳細:配列操作 §.異なる型の配列への複写) |
オーバーラップする範囲のコピー | 適切にコピーされる | 適切にコピーされる |
BlockCopyメソッド以外でメモリブロックのコピーを行う方法、またそのパフォーマンスについては§.メモリブロックのコピー手法とパフォーマンスで別途解説します。
MemoryCopyメソッド
Buffer.MemoryCopyは.NET Framework 4.6より利用可能になったメソッドで、名前が示すとおりmemcpy
に相当するメソッドです(厳密には、よりセキュアな関数memcpy_s
に近い)。
MemoryCopyメソッドはBlockCopyメソッドとは異なり、コピー元・コピー先の配列をポインタで指定します。 MemoryCopyメソッドでは、バッファオーバーランを起こす可能性を低減するために、まず引数destinationSizeInBytesにコピー先の使用可能なサイズを指定します。 その上で、引数sourceBytesToCopyにコピーするバイト数を指定します。
MemoryCopyメソッドではコピー元・コピー先の配列とオフセットをポインタとして指定するため、unsafe
コンテキストで用いる必要があります。
配列⇄アンマネージポインタ間でのコピーを行う場合は、Marshal.Copyメソッドを使うことができます。
MemoryBlockメソッド以外でメモリブロックのコピーを行う方法、またそのパフォーマンスについては§.メモリブロックのコピー手法とパフォーマンスで別途解説します。
バイト列操作に関するクラス
ここではBufferクラス以外でバイト列操作に関連するクラスにどのようなものがあるか紹介します。
プリミティブ型とバイト列の相互変換 (BitConverter)
Bufferクラスのメソッドではプリミティブ型配列の操作を行うことができますが、プリミティブ型の値そのものをバイト列として扱うことはできません。 プリミティブ型の値のバイト列を取得する、また逆にバイト列からプリミティブ型の値に変換する操作を行うにはBitConverterクラスを使います。
バイト列への変換にはBitConverter.GetBytesメソッド、バイト列からの変換にはBitConverter.ToInt32などのメソッドを使います。
プリミティブ型⇄バイト列の相互変換、BitConverterクラスについては基本型の型変換 §.基本型とバイト配列への/からの変換を参照してください。
文字列とバイト列の相互変換 (Encoding)
文字列をバイト列として扱うにはEncodingクラスを使います。 Encodingクラスは特定の文字コードにおける変換規則を表すクラスです。
バイト列への変換にはEncoding.GetBytesメソッド、バイト列からの変換にはEncoding.GetStringメソッドを使います。
構造体とバイト列の相互変換
.NET Frameworkでは任意の構造体とバイト列を直接相互に変換するクラスやメソッドが用意されていません。 独自に実装する方法についてはBinaryReader・BinaryWriterでの構造体の読み書きを参照してください。
バイト列のハッシュ化・暗号化・フォーマット変換
バイト列をMD5, SHA-1, SHA-512などのハッシュ関数でハッシュ化する方法、暗号化する方法、BASE64などにフォーマット変換する方法についてはテキスト変換・フォーマット変換を参照してください。
MemoryStreamとBinaryReader/BinaryWriterを使ったバイト列の相互変換
MemoryStreamとBinaryReader/BinaryWriterを組み合わせて使うことによっても、プリミティブ型とバイト列の相互変換をすることができます。
MemoryStreamについての詳細はMemoryStreamクラス、BinaryReader/BinaryWriterについてはBinaryReaderクラス・BinaryWriterクラスを参照してください。
メモリブロックのコピー手法とパフォーマンス
.NET Frameworkのクラスライブラリで用いることができるメソッドを使って、memcpy
のようなメモリブロックのコピーを行う方法と、そのパフォーマンスについて比較します。 ここでは下記メソッドを検証の対象としています。
- System.Array.Copy
- System.Buffer.BlockCopy
- System.Buffer.MemoryCopy
- System.Runtime.InteropServices.Marshal.Copy
以下は1kBのバッファを10,000,000回コピーした場合と、1MBのバッファを10,000回コピーした場合における各メソッドの所要時間です。 ほぼ同等のスペックではあるものの、それぞれ異なる構成の環境での結果になります。(ランタイムと構成の詳細は後に掲載)
- 結果
- .NET Frameworkでは、どのメソッドでもほぼ同等の速度
- Monoでは、(1)Array.Copy、(2)Buffer.BlockCopyとMarshal.Copy、(3)Buffer.MemoryCopyで速度が異なる(=それぞれ異なる実装になっている?)
- Monoでは、Buffer.MemoryCopyは際立ってパフォーマンスが悪い
- Monoではバッファのサイズが大きくなってもさほど変わらない、.NET Frameworkではバッファのサイズが大きくなると遅くなる
この結果を見る限りでは、Buffer.MemoryCopyやMarshal.Copyによるコピーに大きな優位はないようです。 ただ、マルチプラットフォームでの動作を想定したコードでBuffer.MemoryCopyを使用する場合は、ほかのメソッドを使ったほうがよいか検討する余地がありそうです。
このコードにはSystem.IO.Stream.CopyToによるコピーを掲載しています。 ただ、引数に指定するバッファサイズ次第でパフォーマンスが大きく変わるため、検証結果からは除外しています。