ここでは.NET Frameworkにおいてストリームを扱うクラスであるStreamクラスと、Streamクラスを使ったデータの読み書きといった操作について見ていきます。
ストリームとは
データストリーム、もしくは単にストリームとは、一言で言えばひとつながりのデータ列のことを表します。 データにはその実体が存在する場所や、データが流れてくる経路によって様々なものがあります。 例えば、ディスク上のファイルから読み込まれるデータ、メモリ上から読み取られるデータ、ネットワークを経由して転送されてくるデータ、プロセスの標準入出力を経由して渡されるデータなどがありますが、そういった何らかの経路を経て流れてくるデータ列を総称してストリームと呼びます。
ストリームにおいて重要なのは、データがどのような経路から流れてくるのかといった点を抽象化し、特に流れてくるデータだけに着目できるようにするという点です。 経路や実体を抽象化して単なるデータ列と捉えることにより、様々なデータに対する読み込み・書き込みの操作を画一的な方法で扱えるようになります。
.NET FrameworkにおけるストリームとStreamクラス
.NET Frameworkでは、ストリームを扱うためのクラスとしてStreamクラスが用意されています。 Streamクラスは単にストリームを表すだけでなく、ストリームに対する読み込み・書き込み・シークといった操作が用意されています。
Streamクラスを使ったストリームの読み書きとユーティリティクラス
Streamクラスはすべてのストリームの基本となる抽象型で、より具体的なストリームはこのクラスから継承して実装されます。 例えば、ファイルを開いて読み書きするためのクラスにFileStreamクラスがありますが、これはStreamクラスから派生したものです。
上記の例のようにStreamクラスに用意されているメソッドを使うことによってデータの読み書きができます。 ただ、Streamクラス単体ではバイト単位・バイナリレベルでの読み書きしかできないので、例えば数値を扱うにはBitConverterクラスを使って読み書きの度に型変換を行うといったことをする必要があります。
しかし、それはStreamクラスを単体で使う場合に限ったことで、.NET FrameworkにはStreamクラスを使った数値・文字列の読み書きを簡単に行えるようにする便利なユーティリティクラスが用意されています。 具体的には、StreamReader・StreamWriterやBinaryReader・BinaryWriterといったクラスをStreamクラスと組み合わせて使うことにより、数値・文字列など構造化されたデータをより簡単に読み書きできるようになります。
ここまでの例で挙げたFileStreamはファイルに対する読み書きを行うためのストリームですが、他にもStreamの一種としてバイト配列をストリームとして扱い読み書きを行うためのMemoryStreamが用意されています。
このように、入力ソースがFileStreamであってもMemoryStreamであっても、実際に読み込みを行う部分のコードはどちらも同じです。 StreamReader・StreamWriterやBinaryReader・BinaryWriterといったユーティリティクラスでは、データがファイルとして存在するのか、バイト配列として存在するのかといった違いがあっても、それらがStreamである限りはいずれも同じように扱うことができます。
StreamReader・StreamWriterやBinaryReader・BinaryWriter以外にもStreamを使った読み取り・書き込みをサポートしているクラスは多数あります。 これらのクラスでは、Streamクラスをサポートすることによりファイルやメモリ以外の入力ソースにも幅広く柔軟に対応できるようになっています。
Streamの種類
データソースを抽象化するStream派生クラス
FileStreamやMemoryStream以外にも、.NET Frameworkには各種データソースに対応したStream派生クラスが用意されています。 以下のクラスは、そのようなStream派生クラスの一例です。
クラス | 概要 |
---|---|
FileStream | ファイルシステム上のファイルへの読み書きを行うためのStream (詳細) |
MemoryStream | メモリ上に確保されているバイト配列への読み書きを行うためのStream (詳細) |
UnmanagedMemoryStream | アンマネージブロック上に確保されている領域への読み書きを行うためのStream (詳細) |
NetworkStream | ソケットを使ってデータの送受信を行うためのStream |
データソースの種類によっては具体的なStream派生クラスが公開されていないものも存在しますが、そういったものでも何らかのメソッド呼び出しを利用することでStreamが取得できるものもあります。 Streamを取得するメソッドには次のようなものがあります。
クラス | 概要 |
---|---|
Console.OpenStandardInput
Console.OpenStandardOutput Console.OpenStandardError |
自プロセスの標準ストリームを開いてStreamを取得するためのメソッド |
Assembly.GetManifestResourceStream | アセンブリに埋め込まれたリソースを開いてStreamを取得するためのメソッド |
File.Create
File.Open File.OpenRead File.OpenWrite |
ファイルを開いてFileStreamを取得するためのメソッド (FileStreamのコンストラクタ呼び出しをメソッド形式にして簡略化したもの) |
もちろん、Streamを継承して独自にストリームを実装したり、機能を拡張することも可能です。
データフォーマットの変換やバッファリングなどの機能を追加するStream派生クラス
FileStreamやMemoryStreamといったクラスは各種データソースからの読み書きを実装・抽象化したものですが、Stream派生クラスの一部には他のStreamのラッパーとして動作するものが存在します。 このようなStream派生クラスでは、データソースとなるStreamをラップし、Streamからの読み書きに際してエンコード/デコードや暗号化/復号化などといったデータフォーマットの変換を行うものや、読み書きするデータのバッファリングを行うなどの機能を追加します。 ラッパーとなるStreamでは、データソースに対する読み書き自体は下位のStreamに行わせ、自身は読み書きする側とデータソースの間に入り追加の処理を行います。
例えば次のコードでは、CryptoStreamを使ってデータをBASE64形式にエンコードした上でFileStreamに書き込んでいます。
このように、CryptoStreamを使うことで他のStreamに対してBASE64エンコードを行う機能を追加することができます。
CryptoStreamはBASE64形式でのエンコード・デコード以外にも、様々な形式での暗号化やフォーマット変換を行うために使われます。
上記のコードにおける書き込むデータ・CryptoStreamとFileStream・書き込まれるファイルの関係を図式化すると次のようになります。
逆に、読み込みを行う場合の関係は次のようになります。
CryptoStream以外にも、Streamの読み書きに何らかの機能を追加するラッパーとして動作するStream派生クラスには次のようなものがあります。
クラス | 概要 |
---|---|
BufferedStream | Streamにバッファリングを行う機能を追加するためのStream |
GZipStream
DeflateStream |
Streamにgzip形式・Deflate形式での圧縮・展開機能を追加するためのStream (使用例・解説) |
AuthenticatedStream
SslStream |
StreamにSSLなどのセキュリティ・プロトコルの署名・暗号化機能を追加するためのStream |
CryptoStream | Streamに各種暗号化やフォーマット変換を行う機能を追加するためのStream (使用例) |
これらのクラスは、多段に重ねて使うことも可能です。 例えば、NetworkStreamをSslStreamでラップすることで通信にTLS/SSLを用いるようにし、さらにGZipStreamでラップすることでデータをgzip形式で圧縮してから送信する、といったことができます。
このように、Streamクラスは単純なデータの読み書きからフォーマット変換などの高度な機能の追加まで、様々な目的・方法で汎用的に使用することが出来ます。
Streamクラス
ここからはStreamクラスの基本的な使い方について解説します。 Streamクラスには読み書きを行うメソッドが用意されていますが、これらを直接使って読み書きすることはまれで、ほとんどの場合はStreamReader・StreamWriterやBinaryReader・BinaryWriterで事足ります。
以下の解説中にあるサンプルコードでは、具体例を提示する都合上FileStreamを使っている箇所が多くありますが、特に注記している場合を除いてすべてのStreamに共通する事柄を述べています。 例えば、FileStreamを使っている箇所をMemoryStreamなどに置き換えた場合でも同じように動作します。
読み込み操作
Streamクラスを使ってデータの読み込みを行う方法について。
読み込み (Read, ReadByte)
Streamからデータを読み込むには、Readメソッドを使います。 Readメソッドの引数・戻り値は次のとおりです。
- 第1引数 buffer
- Streamから読み込んだデータを格納するためのバイト配列
- 第2引数 offset
- Streamから読み込んだデータを格納する際の、格納先となるバイト配列中の開始インデックス
- 第3引数 count
- Streamから読み込むデータの最大バイト数
- 戻り値
- 実際にStreamから読み込めたデータのバイト数
つまり、Readメソッドの戻り値をlenとすると、ReadメソッドでStreamから読み込んだデータはbuffer[offset]からbuffer[offset + len - 1]の範囲に格納されることになります。
上記の例で使用しているBitConverter.ToStringメソッドはバイト配列を見やすい形式に変換するためのもので、Readメソッドを使った読み込み処理の本質とは無関係のものです
Readメソッドでは、指定したバイト数の分だけ読み込みを試みますが、指定したバイト数ちょうどのデータが一度に読み込まれるとは限りません。 例えば、読み込んだ結果ストリームの終端に達した場合や、NetworkStreamにおいてデータが送受信の途中だった場合などは、Readメソッドが読み込むバイト数は指定したバイト数よりも少なくなります。
Readメソッドで実際に読み込むことができたデータの長さは、戻り値によって知ることができます。 Streamから読み込めるデータが無くなった場合(すでにストリームの終端に達している場合)は、戻り値として0が返されます。 Readメソッドでの読み込みが成功した場合、読み込めたバイト数だけストリームの現在位置(Position)が移動します。
次の例は、用意したバッファがいっぱいになるまでReadメソッドで読み込みを行う例です。 このコードでは用意したバッファがいっぱいになるか、ストリームの終端に達するまで読み込みを続けます。
ReadByteメソッドを使うと、Streamからデータを1バイトずつ読み込むことができます。 このメソッドでは、読み込めたデータは戻り値として返されます。 ただし、型はint/Integerとなっているため、読み込めたデータはbyte/Byteにキャストして使います。 ReadByteメソッドの呼び出したとき既にストリームの終端に達していている場合は-1が返されます。
Streamクラス自体はバッファリングを行わないため、Peekなどのような先読みを行うメソッド・プロパティは用意されていません。 先読みを行いたい場合はBinaryReader.PeekCharメソッドあるいはStreamReader.Peekメソッドを使います。 バッファリングを行いたい場合はストリームをBufferedStreamクラスでラップします。 Stream派生クラスでは、FileStreamクラスのようにクラスの機能としてバッファリングを行うように実装されている場合もあります。
現在位置と長さ (Position, Length)
ストリーム内の現在位置を取得するにはPositionプロパティを参照します。 この値は、Readメソッド・Writeメソッドで読み書きを行う際の開始位置となります。 読み書きの両方が可能なストリームでも、読み込みと書き込みの開始位置は常に同じとなる(別々ではない)点に注意してください。
また、ストリームの長さを取得するにはLengthプロパティを参照します。
現在位置の変更(シーク)を行うにはPositionプロパティに値を設定するか、Seekメソッドを使います。 また、ストリームの長さを変更するにはSetLengthメソッドを使います。
現在位置や長さを取得できないストリームに対してPosition・Lengthを参照しようとした場合、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリームは長さや位置が取得できないストリームです。
終端のチェック
Streamクラスではバッファリングは行われず、またPeekなどの先読みを行うメソッド・プロパティは用意されないため、読み込みを行った結果ストリームの終端に達したかどうかは事前に知ることはできず、次に読み込みを行ってみるまでわかりません。
Readメソッドでは、ストリームの終端に達している場合に呼び出すと 0 が返されます。 ReadByteメソッドでは -1 が返されます。 Read・ReadByteメソッドは、ストリームの終端に達した後も呼び出すことは可能なので、その戻り値から終端に達したかどうかを判定することが出来ます。 (Streamクラスの読み込みを行うメソッドからは例外EndOfStreamExceptionがスローされることはありません。)
ストリームの現在位置と長さを表すプロパティPositionとLengthの値を調べ、両者の値が同じならストリームの終端に達したと判断することも出来ます。 ただし、NetworkStreamや、Console.OpenStandardInput等のメソッドで取得した標準入出力のストリームなどでは現在位置と長さを取得することはできず、NotSupportedExceptionがスローされてしまうため、この場合はやはりRead・ReadByteメソッドの戻り値を見るほかありません。
StreamReaderクラスにはStreamReader.EndOfStreamプロパティが用意されています。
書き込み操作
Streamクラスを使ってデータの書き込みを行う方法について。
書き込み (Write, WriteByte)
Streamにデータを書き込むには、Writeメソッドを使います。 Writeメソッドの引数は次のとおりです。
- 第1引数 buffer
- Streamに書き込むデータを格納しているバイト配列
- 第2引数 offset
- Streamに書き込むバイト配列中の開始インデックス
- 第3引数 count
- Streamに書き込むデータのバイト数
つまりWriteメソッドでは、buffer[offset]からbuffer[offset + count - 1]の範囲のデータがStreamに書き込まれることになります。
Writeメソッドでの書き込みが成功した場合、書き込めたバイト数だけストリームの現在位置(Position)が移動します。
WriteByteメソッドを使うと、Streamにデータを1バイトずつ書き込むことができます。
フラッシュ (Flush)
Writeメソッドで書き込んだデータをフラッシュさせるには、Flushメソッドを呼び出します。 FileStreamなど、内部でバッファリングが行われるように実装されているStreamでは、Flushメソッドを呼び出すことで内部バッファに格納された内容を反映させることができます。 ただ、Closeメソッドを呼び出したりusingステートメントから抜け出る際には自動的にフラッシュされるので、Streamを閉じる前においてはFlushメソッドを呼び出す必要はありません。
ストリームの長さの設定 (SetLength)
ストリームの長さを変更したい場合は、SetLengthメソッドを呼び出します。 SetLengthメソッドで現在のStreamの長さよりも短くする場合、その内容は切り捨てられます。 同時に、ストリームの書き込み・読み込み位置はストリームの終端に移動します。 逆に現在のStreamの長さよりも長くする場合、拡張した部分の内容は定義されません。 ストリームの読み込み・書き込み位置は変わりません。
Positionプロパティとは異なりLengthプロパティは読み取り専用なので、このプロパティに値を設定してストリームの長さを変更することは出来ません。
長さが変更できないストリームに対してSetLengthを呼び出そうとした場合には、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリーム、固定長に設定されたMemoryStreamなどは長さが変更できないストリームです。
既存のファイルを開いて上書きしようとする場合など、既にストリームに何らかの内容が書き込まれている場合、書き込んだ後にSetLengthでストリームの長さも変更しないと以前の内容が残ります。 例えば、FileStreamで長さ16バイトのファイルを開いて8バイトのデータを書き込む場合、SetLengthでストリームの長さも8バイトに設定しないと、ファイルの長さは16バイトのままになります。
上記の例ではすべての書き込みが終わって長さが確定してからSetLengthメソッドを呼び出していますが、次のようにあらかじめ長さを0にしてストリームの内容を破棄してから書き込むようにすれば、実際に書き込んだ長さを後から調べる必要がなくなり実装を簡略化できます。
さらにFileStreamでは、コンストラクタにFileMode.CreateもしくはFileMode.Truncateを指定してインスタンスを作成すれば、既存のファイルを開いた場合にはその内容は破棄され、ストリームの長さは0となります。 従って、次の例は上記の例と同等の動作となります。
シーク (Seek)
Positionプロパティはストリーム内における現在の読み込み・書き込み位置を取得するためのプロパティですが、ランダムアクセスをサポートするストリームではPositionプロパティに値を設定することでその位置にシークすることができます。
Seekメソッドを使うことでもシークを行うことができます。 Seekメソッドでは、シーク先のオフセットoffsetとシークの原点originの二つを指定します。 originはSeekOrigin列挙体で指定し、その値によってoffsetの意味が次のように変わります。 また、offsetには負の値を指定することもできます。
SeekOrigin | 意味 | 指定例 |
---|---|---|
SeekOrigin.Begin | ストリームの先頭からoffset進めた位置にシークする (Position = offset) |
ストリームの先頭にシークする場合 Seek(0, SeekOrigin.Begin) ストリームの先頭から4バイト目にシークする場合 Seek(4, SeekOrigin.Begin) |
SeekOrigin.Current | ストリーム内の現在の読み込み・書き込み位置からoffset進めた位置にシークする (Position = Position + offset) |
現在位置から8バイト後方にシークする場合 Seek(8, SeekOrigin.Current) 現在位置から8バイト前方にシークする場合 Seek(-8, SeekOrigin.Current) |
SeekOrigin.End | ストリームの終端からoffset進めた位置にシークする (Position = Length + offset) |
ストリームの末尾にシークする場合 Seek(0, SeekOrigin.End) ストリームの末尾から4バイト手前にシークする場合 Seek(-4, SeekOrigin.End) |
次の例は、ストリームの末尾に追記を行うものです。 ストリームを開き、ストリームの末尾にシークしてから書き込みを行うことで、既存の内容の後ろに新たな内容を追記しています。
なおFileStreamでは、コンストラクタにFileMode.Appendを指定してインスタンスを作成すれば、ファイルを開いたあと自動的にストリームの末尾にシークされます。 従って、次の例は上記の例と同等の動作となります。
シーケンシャルアクセスのみをサポートするストリームに対してSeekメソッドを呼び出したり、Positionプロパティに値を設定しようとした場合には、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリームはシークがサポートされないストリームです。
ケーパビリティ (CanRead, CanWrite, CanSeek)
Streamはその種類やコンストラクタでの設定によって書き込み可能か、読み込み可能か異なります。 Streamが書き込み可能かどうかといったケーパビリティを実行時に知るには、CanRead・CanWrite・CanSeekの各プロパティを参照します。
次の例は、様々なStreamに対してそのケーパビリティを調べた例です。
各ケーパビリティと、該当する操作のメソッド・プロパティは次のとおりです。
ケーパビリティのプロパティ | 該当するStreamの操作 |
---|---|
CanWrite | Write, WriteByte, Flush, SetLength |
CanRead | Read, ReadByte |
CanSeek | Seek, SetLength, Position(プロパティの設定) |
ケーパビリティがfalse
になっている操作を行おうとすると、例外NotSupportedExceptionがスローされます。 例えば、CanSeekがfalse
のStreamに対してSeekメソッドやSetLengthメソッドを呼び出そうとすると例外エラーとなります。
上記の例で使用している標準入力・標準出力のStreamの取得については自プロセスの標準入出力 §.標準ストリームの取得で詳しく解説しています。
クローズ (Close, Dispose)
Streamに対する読み書きが終了した後、Closeメソッドを呼び出すことでStreamを閉じることができます。
StreamクラスはIDisposableインターフェイスを実装しているので、Streamをusingステートメント内で使うことが出来ます。 この場合、Closeメソッドを呼び出さなくてもusingステートメントから抜ける時点でそれに相当する処理が自動的に行われます。 そのため、上記のコードと次のコードは、同等のものとなります。
usingステートメントとIDisposableについてはオブジェクトの破棄 §.usingステートメントで詳しく解説しています。
Closeメソッドでストリームを閉じた後は、ほとんどのメソッドの呼び出しとプロパティの参照ができなくなります。 閉じたStreamに対してこれらの操作を行おうとすると例外ObjectDisposedExceptionがスローされます。
ベースとなるストリームのクローズ
GZipStreamなど他のストリームのラッパーとして動作するストリームの場合、そのストリームを閉じる際にベースとなったストリームも一緒に閉じるかどうかを指定することができるものがあります。 コンストラクタの引数leaveOpenにtrueを指定すると、Closeメソッドを呼び出したりusingステートメントから抜けてストリームを閉じた場合でも、ベースとなったストリームは開いたままになります。
次の例では、GZipStreamを使ってメモリ上で圧縮・展開を行なっていますが、圧縮と展開で同じMemoryStreamを使えるようleaveOpenにtrueを指定してGZipStreamを作成しています。
なお、StreamReader・StreamWriterやBinaryReader・BinaryWriterでもベースとなったストリームを開いたままにするかどうかを指定することができます。
コピー (CopyTo)
ストリームの内容を別のストリームにコピーするには、CopyToメソッドを使うことができます。 あるストリームの内容をコピーしてメモリ上(MemoryStream)に保持したり、ファイル(FileStream)に書き出したりしたい場合などに使えます。
CopyToメソッドでは、コピー元のStreamからReadしたものをコピー先のStreamにWriteします。 この際、コピー元・コピー先ともに現在位置からのコピーが行われます。 つまり現在位置がストリームの先頭でない場合、ストリームの途中からコピーが行われます。 コピー開始時にストリームの先頭へシークされることはありません。 また、CopyToメソッドではコピーする長さを指定することはできず、常にストリームの終端までがコピーされます。
CopyToメソッドでのコピーの際、CopyToメソッド内でデータの読み書きに使用されるバッファが確保されますが、このバッファのサイズを指定することもできます。 なお、指定しなかった場合ではデフォルトで4096バイトのバッファが確保されます。
CopyToメソッドは.NET Framework 4以降で使用可能なメソッドです。 .NET Framework 3.5以前の場合はCopyToメソッドを使うことは出来ないので、次のようにReadメソッド・Writeメソッドを使ってコピー処理を実装する必要があります。
非同期操作
(未整理)
BeginRead・BeginWriteなどのメソッドを使うことで、Streamを使って非同期の読み書きを行うことが出来ます。
非同期呼び出しとコールバックについてはデリゲートの機能 §.メソッドの非同期呼び出し (BeginInvoke, EndInvoke)を参照してください。
ストリームの種類によっては操作のタイムアウトもサポートしています。 CanTimeoutプロパティを参照することで操作がタイムアウト可能かどうかを知ることができます。 また、ReadTimeoutプロパティ・WriteTimeoutプロパティでタイムアウト時間の取得・設定ができます。
複数のスレッドからStreamにアクセスする場合、Synchronizedメソッドメソッドを使うとStreamに対する読み書き操作をスレッドセーフにすることができます。
.NET Framework 4.5からは、ReadAsync・WriteAsyncといった非同期操作のメソッドも使えるようになっています。
ヌルオブジェクト (Null)
Streamクラスにはヌルオブジェクトを取得するプロパティStream.Nullが用意されています。 Stream.Nullは、NULLデバイス(nul・/dev/null)の代わりとして使ったり、テスト時に具体的なStreamを用意する代わりにモックとして使用したりすることができます。
Stream.Nullは読み込み・書き込み・シークなど全ての機能をサポートしますが、実際にそれらの操作を行なっても何も起こりません。 Stream.Nullに対してWriteメソッドで書き込んだ内容はすべて破棄され、ReadメソッドでStream.Nullから読み込みを行った場合は0バイトのデータが読み出されます。 Stream.Nullの長さ(Length)は常に0です。
次の例では、出力先をStream.Nullに設定したStreamWriterをConsole.SetOutメソッド渡すことによって標準出力の出力先をコンソールからStream.Nullに変更(リダイレクト)し、Console.WriteLineメソッドで出力される内容を破棄しています。
標準出力のリダイレクトについては自プロセスの標準入出力 §.標準ストリームのリダイレクトで詳しく解説しています。
構造体の読み書き
Streamクラスには任意の構造体を読み書きするメソッドは用意されていません。 ストリーム中のブロックを構造体として読み書きしたい場合、fread
/fwrite
関数のような読み書きを行いたい場合は、いったんバイト配列として読み書きし、さらにバイト配列から構造体に変換する必要があります。 具体的な実装例についてはBinaryReader・BinaryWriterでの構造体の読み書きで紹介しています。