ここでは.NET Frameworkにおいてストリームを扱うクラスであるStreamクラスと、Streamクラスを使ったデータの読み書きといった操作について見ていきます。

§1 ストリームとは

データストリーム、もしくは単にストリームとは、一言で言えばひとつながりのデータ列のことを表します。 データにはその実体が存在する場所や、データが流れてくる経路によって様々なものがあります。 例えば、ディスク上のファイルから読み込まれるデータ、メモリ上から読み取られるデータ、ネットワークを経由して転送されてくるデータ、プロセスの標準入出力を経由して渡されるデータなどがありますが、そういった何らかの経路を経て流れてくるデータ列を総称してストリームと呼びます。

ストリームにおいて重要なのは、データがどのような経路から流れてくるのかといった点を抽象化し、特に流れてくるデータだけに着目できるようにするという点です。 経路や実体を抽象化して単なるデータ列と捉えることにより、様々なデータに対する読み込み・書き込みの操作を画一的な方法で扱えるようになります。



§2 .NET FrameworkにおけるストリームとStreamクラス

.NET Frameworkでは、ストリームを扱うためのクラスとしてStreamクラスが用意されています。 Streamクラスは単にストリームを表すだけでなく、ストリームに対する読み込み・書き込み・シークといった操作が用意されています。

§2.1 Streamクラスを使ったストリームの読み書きとユーティリティクラス

Streamクラスはすべてのストリームの基本となる抽象型で、より具体的なストリームはこのクラスから継承して実装されます。 例えば、ファイルを開いて読み書きするためのクラスにFileStreamクラスがありますが、これはStreamクラスから派生したものです。

FileStreamを使ってファイルからデータを読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.datを開き、読み取りアクセスを行うためのFileStreamを作成する
    using (FileStream stream = new FileStream("sample.dat", FileMode.Open, FileAccess.Read)) {
      // 読み込んだデータを格納するためのバッファ
      byte[] buffer = new byte[4];

      // FileStreamから4バイト読み込みバッファに格納する
      stream.Read(buffer, 0, 4);

      // 読み込んだデータをInt32(int)に変換して表示する
      int number = BitConverter.ToInt32(buffer, 0);

      Console.WriteLine(number);
    }
  }
}

上記の例のようにStreamクラスに用意されているメソッドを使うことによってデータの読み書きができます。 ただ、Streamクラス単体ではバイト単位・バイナリレベルでの読み書きしかできないので、例えば数値を扱うにはBitConverterクラスを使って読み書きの度に型変換を行うといったことをする必要があります。


しかし、それはStreamクラスを単体で使う場合に限ったことで、.NET FrameworkにはStreamクラスを使った数値・文字列の読み書きを簡単に行えるようにする便利なユーティリティクラスが用意されています。 具体的には、StreamReader・StreamWriterBinaryReader・BinaryWriterといったクラスをStreamクラスと組み合わせて使うことにより、数値・文字列など構造化されたデータをより簡単に読み書きできるようになります。

FileStreamとBinaryReaderを使ってファイルから数値を読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.datを開き、読み取りアクセスを行うためのFileStreamを作成する
    using (FileStream stream = new FileStream("sample.dat", FileMode.Open, FileAccess.Read)) {
      // streamからデータを読み出すBinaryReaderを作成する
      BinaryReader reader = new BinaryReader(stream);

      // streamからInt32(int)のデータを読み込み、表示する
      int number = reader.ReadInt32();

      Console.WriteLine(number);
    }
  }
}
FileStreamとStreamReaderを使ってファイルから文字列を読み込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // ファイルsample.txtを開き、読み取りアクセスを行うためのFileStreamを作成する
    using (FileStream stream = new FileStream("sample.txt", FileMode.Open, FileAccess.Read)) {
      // テキストエンコーディングにUTF-8を用いてstreamの読み込みを行うStreamReaderを作成する
      StreamReader reader = new StreamReader(stream, Encoding.UTF8);

      // streamから文字列を一行分読み込み、表示する
      string line = reader.ReadLine();

      Console.WriteLine(line);
    }
  }
}

ここまでの例で挙げたFileStreamはファイルに対する読み書きを行うためのストリームですが、他にもStreamの一種としてバイト配列をストリームとして扱い読み書きを行うためのMemoryStreamが用意されています。

MemoryStreamとStreamReaderを使ってバイト配列から文字列を読み込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // 何らかのデータが格納されているバイト配列を想定
    byte[] data = new byte[32];

    // バイト配列を読み取り専用のストリームとして扱うMemoryStreamを作成する
    using (MemoryStream stream = new MemoryStream(data, false)) {
      // テキストエンコーディングにUTF-8を用いてstreamの読み込みを行うStreamReaderを作成
      StreamReader reader = new StreamReader(stream, Encoding.UTF8);

      // streamから文字列を一行分読み込み、表示する
      string line = reader.ReadLine();

      Console.WriteLine(line);
    }
  }
}

このように、入力ソースがFileStreamであってもMemoryStreamであっても、実際に読み込みを行う部分のコードはどちらも同じです。 StreamReader・StreamWriterやBinaryReader・BinaryWriterといったユーティリティクラスでは、データがファイルとして存在するのか、バイト配列として存在するのかといった違いがあっても、それらがStreamである限りはいずれも同じように扱うことができます。

StreamReader・StreamWriterやBinaryReader・BinaryWriter以外にもStreamを使った読み取り・書き込みをサポートしているクラスは多数あります。 これらのクラスでは、Streamクラスをサポートすることによりファイルやメモリ以外の入力ソースにも幅広く柔軟に対応できるようになっています。

§2.2 Streamの種類

§2.2.1 データソースを抽象化するStream派生クラス

FileStreamやMemoryStream以外にも、.NET Frameworkには各種データソースに対応したStream派生クラスが用意されています。 以下のクラスは、そのようなStream派生クラスの一例です。

Streamの派生クラス (一例)
クラス 概要
FileStream ファイルシステム上のファイルへの読み書きを行うためのStream (詳細)
MemoryStream メモリ上に確保されているバイト配列への読み書きを行うためのStream (詳細)
UnmanagedMemoryStream アンマネージブロック上に確保されている領域への読み書きを行うためのStream (詳細)
NetworkStream ソケットを使ってデータの送受信を行うためのStream

データソースの種類によっては具体的な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を継承して独自にストリームを実装したり、機能を拡張することも可能です。

§2.2.2 データフォーマットの変換やバッファリングなどの機能を追加するStream派生クラス

FileStreamやMemoryStreamといったクラスは各種データソースからの読み書きを実装・抽象化したものですが、Stream派生クラスの一部には他のStreamのラッパーとして動作するものが存在します。 このようなStream派生クラスでは、データソースとなるStreamをラップし、Streamからの読み書きに際してエンコード/デコードや暗号化/復号化などといったデータフォーマットの変換を行うものや、読み書きするデータのバッファリングを行うなどの機能を追加します。 ラッパーとなるStreamでは、データソースに対する読み書き自体は下位のStreamに行わせ、自身は読み書きする側とデータソースの間に入り追加の処理を行います。

例えば次のコードでは、CryptoStreamを使ってデータをBASE64形式にエンコードした上でFileStreamに書き込んでいます。

CryptoStreamを使ってデータをBASE64エンコードしてFileStreamに書き込む
using System;
using System.IO;
using System.Security.Cryptography;

class Sample {
  static void Main()
  {
    // ファイルsample.datを開き、書き込みアクセスを行うためのFileStreamを作成する
    using (Stream fileStream = new FileStream("sample.dat", FileMode.Create, FileAccess.Write)) {
      // BASE64への変換を行った上でfileStreamに書き込みを行うCryptoStreamを作成する
      using (Stream base64Stream = new CryptoStream(fileStream, new ToBase64Transform(), CryptoStreamMode.Write)) {
        // 書き込むデータが格納されているバイト配列
        byte[] data = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

        // データを書き込む
        // CryptoStreamによりデータはBASE64にエンコードされ、次いで
        // FileStreamによりエンコードされたデータがファイルに書き込まれる
        base64Stream.Write(data, 0, 8);
      }
    }
  }
}
sample.datに書き込まれる内容
QUJDREVGR0g=

このように、CryptoStreamを使うことで他のStreamに対してBASE64エンコードを行う機能を追加することができます。

CryptoStreamはBASE64形式でのエンコード・デコード以外にも、様々な形式での暗号化やフォーマット変換を行うために使われます。


上記のコードにおける書き込むデータ・CryptoStreamとFileStream・書き込まれるファイルの関係を図式化すると次のようになります。

書き込むデータ・CryptoStreamとFileStream・ファイルの関係
[データ data]
    ↓
CryptoStream
(BASE64へエンコードする)
    ↓
FileStream
(ファイルへ書き込む)
    ↓
[ファイル sample.dat]

逆に、読み込みを行う場合の関係は次のようになります。

読み込まれるデータ・CryptoStreamとFileStream・ファイルの関係
[データ data]
    ↑
CryptoStream
(BASE64からデコードする)
    ↑
FileStream
(ファイルから読み込む)
    ↑
[ファイル sample.dat]

CryptoStream以外にも、Streamの読み書きに何らかの機能を追加するラッパーとして動作する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を多段に重ねて使う例
[データ]
    ↓
GZipStream
(gzip形式で圧縮する)
    ↓
SslStream
(TLS/SSLによる暗号化を行う)
    ↓
NetworkStream
(ソケットを使って送信する)
    ↓
[ネットワーク]

このように、Streamクラスは単純なデータの読み書きからフォーマット変換などの高度な機能の追加まで、様々な目的・方法で汎用的に使用することが出来ます。

§3 Streamクラス

ここからはStreamクラスの基本的な使い方について解説します。 Streamクラスには読み書きを行うメソッドが用意されていますが、これらを直接使って読み書きすることはまれで、ほとんどの場合はStreamReader・StreamWriterBinaryReader・BinaryWriterで事足ります。

以下の解説中にあるサンプルコードでは、具体例を提示する都合上FileStreamを使っている箇所が多くありますが、特に注記している場合を除いてすべてのStreamに共通する事柄を述べています。 例えば、FileStreamを使っている箇所をMemoryStreamなどに置き換えた場合でも同じように動作します。

§3.1 読み込み操作

Streamクラスを使ってデータの読み込みを行う方法について。

§3.1.1 読み込み (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]の範囲に格納されることになります。

Readメソッドを使ってStreamからデータを読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      // 読み込んだデータを格納するためのバッファ
      byte[] buffer = new byte[8];

      // streamから最大4バイトを読み込み、buffer[0]以降に格納する
      int len = stream.Read(buffer, 0, 4);

      // 実際に読み込めた分を表示する
      Console.WriteLine("{0} {1}", len, BitConverter.ToString(buffer, 0, len));

      // streamから最大2バイトを読み込み、buffer[4]以降に格納する
      len = stream.Read(buffer, 4, 2);

      // 実際に読み込めた分を表示する
      Console.WriteLine("{0} {1}", len, BitConverter.ToString(buffer, 4, len));
    }
  }
}

上記の例で使用しているBitConverter.ToStringメソッドはバイト配列を見やすい形式に変換するためのもので、Readメソッドを使った読み込み処理の本質とは無関係のものです

Readメソッドでは、指定したバイト数の分だけ読み込みを試みますが、指定したバイト数ちょうどのデータが一度に読み込まれるとは限りません。 例えば、読み込んだ結果ストリームの終端に達した場合や、NetworkStreamにおいてデータが送受信の途中だった場合などは、Readメソッドが読み込むバイト数は指定したバイト数よりも少なくなります。


Readメソッドで実際に読み込むことができたデータの長さは、戻り値によって知ることができます。 Streamから読み込めるデータが無くなった場合(すでにストリームの終端に達している場合)は、戻り値として0が返されます。 Readメソッドでの読み込みが成功した場合、読み込めたバイト数だけストリームの現在位置(Position)が移動します。

次の例は、用意したバッファがいっぱいになるまでReadメソッドで読み込みを行う例です。 このコードでは用意したバッファがいっぱいになるか、ストリームの終端に達するまで読み込みを続けます。

Readメソッドを使ってバッファがいっぱいになるか終端に達するまでStreamからデータを読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      byte[] buffer = new byte[16]; // 読み込んだデータを格納するためのバッファ
      int offset = 0;               // 読み込んだデータを格納する位置の初期値
      int count = buffer.Length;    // 読み込むバイト数の初期値

      for (;;) {
        // 最大countバイト読み込み、bufferのインデックスoffset以降に格納する
        int len = stream.Read(buffer, offset, count);

        if (len == 0) {
          break; // これ以上読み込めるデータが無いので、読み込みを終了する
        }
        else {
          offset += len;  // 読み込めた長さの分だけ、次回のReadでバッファに格納する位置を移動する
          count -= len;   // 読み込めた長さの分だけ、次回のReadで読み込むバイト数を減らす
        }
      }

      // 実際に読み込めた分を表示する
      Console.WriteLine(BitConverter.ToString(buffer, 0, offset));
    }
  }
}

ReadByteメソッドを使うと、Streamからデータを1バイトずつ読み込むことができます。 このメソッドでは、読み込めたデータは戻り値として返されます。 ただし、型はint/Integerとなっているため、読み込めたデータはbyte/Byteにキャストして使います。 ReadByteメソッドの呼び出したとき既にストリームの終端に達していている場合は-1が返されます。

ReadByteメソッドを使ってStreamから1バイトずつデータを読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      for (;;) {
        // streamから1バイト読み込む
        int data = stream.ReadByte();

        if (data == -1)
          // 終端に達した
          break;

        // byteにキャストして表示
        byte b = (byte)data;

        Console.Write("{0:x2} ", b);
      }

      Console.WriteLine();
    }
  }
}

Streamクラス自体はバッファリングを行わないため、Peekなどのような先読みを行うメソッド・プロパティは用意されていません。 先読みを行いたい場合はBinaryReader.PeekCharメソッドあるいはStreamReader.Peekメソッドを使います。 バッファリングを行いたい場合はストリームをBufferedStreamクラスでラップします。 Stream派生クラスでは、FileStreamクラスのようにクラスの機能としてバッファリングを行うように実装されている場合もあります。

§3.1.2 現在位置と長さ (Position, Length)

ストリーム内の現在位置を取得するにはPositionプロパティを参照します。 この値は、ReadメソッドWriteメソッドで読み書きを行う際の開始位置となります。 読み書きの両方が可能なストリームでも、読み込みと書き込みの開始位置は常に同じとなる(別々ではない)点に注意してください。

また、ストリームの長さを取得するにはLengthプロパティを参照します。

Streamの長さとStream内の現在位置を取得する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.dat")) {
      // streamの長さと現在の位置を表示
      Console.WriteLine("Length : {0}", stream.Length);
      Console.WriteLine("Position : {0}", stream.Position);

      // streamからデータを読み込む
      byte[] buffer = new byte[8];

      int len = stream.Read(buffer, 0, buffer.Length);

      // 読み込めたバイト数と現在の位置を表示
      Console.WriteLine("Read() : {0}", len);
      Console.WriteLine("Position : {0}", stream.Position);
    }
  }
}
実行結果例
Length : 32
Position : 0
Read() : 8
Position : 8

現在位置の変更(シーク)を行うにはPositionプロパティに値を設定するか、Seekメソッドを使います。 また、ストリームの長さを変更するにはSetLengthメソッドを使います。

現在位置や長さを取得できないストリームに対してPosition・Lengthを参照しようとした場合、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリームは長さや位置が取得できないストリームです。

§3.1.3 終端のチェック

Streamクラスではバッファリングは行われず、またPeekなどの先読みを行うメソッド・プロパティは用意されないため、読み込みを行った結果ストリームの終端に達したかどうかは事前に知ることはできず、次に読み込みを行ってみるまでわかりません

Readメソッドでは、ストリームの終端に達している場合に呼び出すと 0 が返されます。 ReadByteメソッドでは -1 が返されます。 Read・ReadByteメソッドは、ストリームの終端に達した後も呼び出すことは可能なので、その戻り値から終端に達したかどうかを判定することが出来ます。 (Streamクラスの読み込みを行うメソッドからは例外EndOfStreamExceptionがスローされることはありません。)

ストリームの現在位置と長さを表すプロパティPositionとLengthの値を調べ、両者の値が同じならストリームの終端に達したと判断することも出来ます。 ただし、NetworkStreamや、Console.OpenStandardInput等のメソッドで取得した標準入出力のストリームなどでは現在位置と長さを取得することはできず、NotSupportedExceptionがスローされてしまうため、この場合はやはりRead・ReadByteメソッドの戻り値を見るほかありません。

§3.2 書き込み操作

Streamクラスを使ってデータの書き込みを行う方法について。

§3.2.1 書き込み (Write, WriteByte)

Streamにデータを書き込むには、Writeメソッドを使います。 Writeメソッドの引数は次のとおりです。

第1引数 buffer
Streamに書き込むデータを格納しているバイト配列
第2引数 offset
Streamに書き込むバイト配列中の開始インデックス
第3引数 count
Streamに書き込むデータのバイト数

つまりWriteメソッドでは、buffer[offset]からbuffer[offset + count - 1]の範囲のデータがStreamに書き込まれることになります。

Writeメソッドを使ってStreamにデータを書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの0番目から4バイト分(buffer[0]〜buffer[3])をstreamに書き込む
      stream.Write(buffer, 0, 4);

      // bufferの4番目から4バイト分(buffer[4]〜buffer[7])をstreamに書き込む
      stream.Write(buffer, 4, 4);
    }
  }
}

Writeメソッドでの書き込みが成功した場合、書き込めたバイト数だけストリームの現在位置(Position)が移動します。


WriteByteメソッドを使うと、Streamにデータを1バイトずつ書き込むことができます。

WriteByteメソッドを使ってStreamに1バイトずつデータを書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 書き込むデータが格納されているバイト配列
      byte[] data = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      for (int index = 0; index < data.Length; index++) {
        // streamに1バイトずつ書き込む
        stream.WriteByte(data[index]);
      }
    }
  }
}

§3.2.2 フラッシュ (Flush)

Writeメソッドで書き込んだデータをフラッシュさせるには、Flushメソッドを呼び出します。 FileStreamなど、内部でバッファリングが行われるように実装されているStreamでは、Flushメソッドを呼び出すことで内部バッファに格納された内容を反映させることができます。 ただ、Closeメソッドを呼び出したりusingステートメントから抜け出る際には自動的にフラッシュされるので、Streamを閉じる前においてはFlushメソッドを呼び出す必要はありません。

Flushメソッドを使ってStreamに書き込んだ内容をフラッシュする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの0番目から4バイト分(buffer[0]〜buffer[3])をstreamに書き込む
      stream.Write(buffer, 0, 4);

      // 書き込んだ内容をフラッシュする
      stream.Flush();

      // bufferの4番目から4バイト分(buffer[4]〜buffer[7])をstreamに書き込む
      stream.Write(buffer, 4, 4);
    }
    // usingステートメントから抜け出る際にもDisposeメソッドにより自動的にフラッシュされる
  }
}

§3.2.3 ストリームの長さの設定 (SetLength)

ストリームの長さを変更したい場合は、SetLengthメソッドを呼び出します。 SetLengthメソッドで現在のStreamの長さよりも短くする場合、その内容は切り捨てられます。 同時に、ストリームの書き込み・読み込み位置はストリームの終端に移動します。 逆に現在のStreamの長さよりも長くする場合、拡張した部分の内容は定義されません。 ストリームの読み込み・書き込み位置は変わりません。

SetLengthメソッドを使ってStreamの長さを変更する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);

      // streamの長さを4バイトにする
      stream.SetLength(4);
    }
  }
}

Positionプロパティとは異なりLengthプロパティは読み取り専用なので、このプロパティに値を設定してストリームの長さを変更することは出来ません。

長さが変更できないストリームに対してSetLengthを呼び出そうとした場合には、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリーム、固定長に設定されたMemoryStreamなどは長さが変更できないストリームです。


既存のファイルを開いて上書きしようとする場合など、既にストリームに何らかの内容が書き込まれている場合、書き込んだ後にSetLengthでストリームの長さも変更しないと以前の内容が残ります。 例えば、FileStreamで長さ16バイトのファイルを開いて8バイトのデータを書き込む場合、SetLengthでストリームの長さも8バイトに設定しないと、ファイルの長さは16バイトのままになります。

SetLengthメソッドを使ってStreamの長さを書き込んだ長さに合わせて変更する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 現在のstreamの長さを表示
      Console.WriteLine(stream.Length);

      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);

      Console.WriteLine(stream.Length);

      // 現在の書き込み位置(8バイト目)をstreamの長さとして設定する
      stream.SetLength(stream.Position);

      Console.WriteLine(stream.Length);
    }
  }
}

上記の例ではすべての書き込みが終わって長さが確定してからSetLengthメソッドを呼び出していますが、次のようにあらかじめ長さを0にしてストリームの内容を破棄してから書き込むようにすれば、実際に書き込んだ長さを後から調べる必要がなくなり実装を簡略化できます。

Streamの長さをいったん0に設定して内容を破棄してから書き込みを行う
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // 現在のstreamの長さを表示
      Console.WriteLine(stream.Length);

      // streamの長さをいったん0にして現在の内容を破棄する
      stream.SetLength(0);

      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);

      Console.WriteLine(stream.Length);
    }
  }
}

さらにFileStreamでは、コンストラクタにFileMode.CreateもしくはFileMode.Truncateを指定してインスタンスを作成すれば、既存のファイルを開いた場合にはその内容は破棄され、ストリームの長さは0となります。 従って、次の例は上記の例と同等の動作となります。

FileStreamを作成する際に既存の内容を破棄してから書き込みを行う
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // FileMode.Createを指定してFileStreamを作成
    using (Stream stream = new FileStream("sample.dat", FileMode.Create, FileAccess.Write)) {
      // 現在のstreamの長さを表示
      Console.WriteLine(stream.Length);

      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);

      Console.WriteLine(stream.Length);
    }
  }
}
実行結果
D:\test>type sample.dat
XXXXXXXXXXXXXXXX
D:\test>Sample.exe
0
8

D:\test>type sample.dat
ABCDEFGH

§3.3 シーク (Seek)

Positionプロパティはストリーム内における現在の読み込み・書き込み位置を取得するためのプロパティですが、ランダムアクセスをサポートするストリームではPositionプロパティに値を設定することでその位置にシークすることができます。

Positionプロパティに値を設定してStreamをシークする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // ストリームの書き込み位置を4バイト目に移動する
      stream.Position = 4;

      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);

      // ストリームの書き込み位置を0バイト目(ストリームの先頭)に移動する
      stream.Position = 0;

      // bufferの4バイト分をstreamに書き込む
      stream.Write(buffer, 0, 4);
    }
  }
}

Seekメソッドを使うことでもシークを行うことができます。 Seekメソッドでは、シーク先のオフセットoffsetとシークの原点originの二つを指定します。 originSeekOrigin列挙体で指定し、その値によってoffsetの意味が次のように変わります。 また、offsetには負の値を指定することもできます。

SeekOriginとSeekメソッドの移動結果の違い
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)

次の例は、ストリームの末尾に追記を行うものです。 ストリームを開き、ストリームの末尾にシークしてから書き込みを行うことで、既存の内容の後ろに新たな内容を追記しています。

Seekメソッドを使ってStreamの末尾にシークしてから内容を追記する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample.dat")) {
      // ストリームの末尾までシーク
      stream.Seek(0, SeekOrigin.End);

      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);
    }
  }
}

なおFileStreamでは、コンストラクタにFileMode.Appendを指定してインスタンスを作成すれば、ファイルを開いたあと自動的にストリームの末尾にシークされます。 従って、次の例は上記の例と同等の動作となります。

FileStreamを作成する際に末尾にシークしてから内容を追記する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // FileMode.Appendを指定してFileStreamを作成
    using (Stream stream = new FileStream("sample.dat", FileMode.Append, FileAccess.Write)) {
      // 書き込むデータが格納されているバイト配列
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // bufferの8バイト分をstreamに書き込む
      stream.Write(buffer, 0, 8);
    }
  }
}

シーケンシャルアクセスのみをサポートするストリームに対してSeekメソッドを呼び出したり、Positionプロパティに値を設定しようとした場合には、例外NotSupportedExceptionがスローされます。 例えば、NetworkStreamや標準入出力のストリームはシークがサポートされないストリームです。

§3.4 ケーパビリティ (CanRead, CanWrite, CanSeek)

Streamはその種類やコンストラクタでの設定によって書き込み可能か、読み込み可能か異なります。 Streamが書き込み可能かどうかといったケーパビリティを実行時に知るには、CanReadCanWriteCanSeekの各プロパティを参照します。

次の例は、様々なStreamに対してそのケーパビリティを調べた例です。

ストリームが読み込み可能・書き込み可能・シーク可能か調べる
using System;
using System.IO;

class Sample {
  static void PrintStreamCapability(Stream stream)
  {
    Console.WriteLine("CanRead  : {0}", stream.CanRead);
    Console.WriteLine("CanWrite : {0}", stream.CanWrite);
    Console.WriteLine("CanSeek  : {0}", stream.CanSeek);
  }

  static void Main()
  {
    // 読み込み用に開いたFileStream
    Console.WriteLine("[File.OpenRead]");
    using (Stream stream = File.OpenRead("sample.dat")) {
      PrintStreamCapability(stream);
    }

    // 書き込み用に開いたFileStream
    Console.WriteLine("[File.OpenWrite]");
    using (Stream stream = File.OpenWrite("sample.dat")) {
      PrintStreamCapability(stream);
    }

    // 書き込み可能なMemoryStream
    Console.WriteLine("[MemoryStream]");
    using (Stream stream = new MemoryStream()) {
      PrintStreamCapability(stream);
    }

    // 読み込み専用のMemoryStream
    Console.WriteLine("[MemoryStream (read only)]");
    using (Stream stream = new MemoryStream(new byte[8], false)) {
      PrintStreamCapability(stream);
    }

    // 標準入力のStream
    Console.WriteLine("[Console.OpenStandardInput]");
    using (Stream stream = Console.OpenStandardInput()) {
      PrintStreamCapability(stream);
    }

    // 標準出力のStream
    Console.WriteLine("[Console.OpenStandardOutput]");
    using (Stream stream = Console.OpenStandardOutput()) {
      PrintStreamCapability(stream);
    }
  }
}
実行結果
[File.OpenRead]
CanRead  : True
CanWrite : False
CanSeek  : True
[File.OpenWrite]
CanRead  : False
CanWrite : True
CanSeek  : True
[MemoryStream]
CanRead  : True
CanWrite : True
CanSeek  : True
[MemoryStream (read only)]
CanRead  : True
CanWrite : False
CanSeek  : True
[Console.OpenStandardInput]
CanRead  : True
CanWrite : False
CanSeek  : False
[Console.OpenStandardOutput]
CanRead  : False
CanWrite : True
CanSeek  : False

各ケーパビリティと、該当する操作のメソッド・プロパティは次のとおりです。

各ケーパビリティと該当する操作
ケーパビリティのプロパティ 該当するStreamの操作
CanWrite Write, WriteByte, Flush, SetLength
CanRead Read, ReadByte
CanSeek Seek, SetLength, Position(プロパティの設定)

ケーパビリティがfalseになっている操作を行おうとすると、例外NotSupportedExceptionがスローされます。 例えば、CanSeekがfalseのStreamに対してSeekメソッドやSetLengthメソッドを呼び出そうとすると例外エラーとなります。

上記の例で使用している標準入力・標準出力のStreamの取得については自プロセスの標準入出力 §.標準ストリームの取得で詳しく解説しています。

§3.5 クローズ (Close, Dispose)

Streamに対する読み書きが終了した後、Closeメソッドを呼び出すことでStreamを閉じることができます。

Closeメソッドを使ってStreamを閉じる
using System;
using System.IO;

class Sample {
  static void Main()
  {
    Stream stream = null;

    try {
      // Streamを開く
      stream = File.OpenWrite("sample.dat");

      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // streamにデータを書き込む
      stream.Write(buffer, 0, 8);
    }
    finally {
      // すべての処理が終わったらstreamを閉じる
      if (stream != null)
        stream.Close();
    }
  }
}

StreamクラスはIDisposableインターフェイスを実装しているので、Streamをusingステートメント内で使うことが出来ます。 この場合、Closeメソッドを呼び出さなくてもusingステートメントから抜ける時点でそれに相当する処理が自動的に行われます。 そのため、上記のコードと次のコードは、同等のものとなります。

usingステートメントを使ってStreamを閉じる
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // Streamを開き、usingステートメント内で使用する
    using (Stream stream = File.OpenWrite("sample.dat")) {
      byte[] buffer = new byte[8] {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48};

      // streamにデータを書き込む
      stream.Write(buffer, 0, 8);
    }
    // usingステートメントを抜ける時点でCloseメソッドに相当する処理が行われ、streamは閉じられる
  }
}

usingステートメントとIDisposableについてはオブジェクトの破棄 §.usingステートメントで詳しく解説しています。

Closeメソッドでストリームを閉じた後は、ほとんどのメソッドの呼び出しとプロパティの参照ができなくなります。 閉じたStreamに対してこれらの操作を行おうとすると例外ObjectDisposedExceptionがスローされます。

§3.5.1 ベースとなるストリームのクローズ

GZipStreamなど他のストリームのラッパーとして動作するストリームの場合、そのストリームを閉じる際にベースとなったストリームも一緒に閉じるかどうかを指定することができるものがあります。 コンストラクタの引数leaveOpenにtrueを指定すると、Closeメソッドを呼び出したりusingステートメントから抜けてストリームを閉じた場合でも、ベースとなったストリームは開いたままになります。

次の例では、GZipStreamを使ってメモリ上で圧縮・展開を行なっていますが、圧縮と展開で同じMemoryStreamを使えるようleaveOpenにtrueを指定してGZipStreamを作成しています。

ラッパーとなるストリームを閉じる際にベースとなったストリームを開いたままにする
using System;
using System.IO;
using System.IO.Compression;

class Sample {
  static void Main()
  {
    // GZipStreamで圧縮した結果を書き込むためのMemoryStreamを作成する
    using (Stream compressedStream = new MemoryStream()) {
      // データを圧縮してcompressedStreamに書き込むためのGZipStreamを作成する
      // (GZipStreamを閉じてもcompressedStreamは閉じないようleaveOpenをtrueにする)
      using (Stream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress, true)) {
        // 圧縮するファイルのFileStreamを作成する
        using (Stream inputStream = File.OpenRead("sample.txt")) {
          // inputStreamからデータを読み出し、gzipStreamに書き込む
          inputStream.CopyTo(gzipStream);
        }
      }
      // この時点でgzipStreamは閉じられるが、compressedStreamは引き続き使用できる

      // compressedStreamの現在位置をストリームの先頭に戻し、GZipStreamで再度展開する
      compressedStream.Position = 0L;

      using (Stream gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress, false)) {
        // StreamReaderを使って展開した内容を読み込んで表示する
        StreamReader reader = new StreamReader(gzipStream);

        Console.WriteLine(reader.ReadToEnd());
      }
      // この時点でgzipStreamは閉じられるが、leaveOpenにfalseを指定しているため、
      // compressedStreamも閉じられる
    }
  }
}

なお、StreamReader・StreamWriterBinaryReader・BinaryWriterでもベースとなったストリームを開いたままにするかどうかを指定することができます。

§3.6 コピー (CopyTo)

ストリームの内容を別のストリームにコピーするには、CopyToメソッドを使うことができます。 あるストリームの内容をコピーしてメモリ上(MemoryStream)に保持したり、ファイル(FileStream)に書き出したりしたい場合などに使えます。

CopyToメソッドを使ってStreamの内容を別のStreamにコピーする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream fromStream = File.OpenRead("sample.dat")) {
      using (MemoryStream toStream = new MemoryStream()) {
        // ファイルの内容をMemoryStreamにコピー
        fromStream.CopyTo(toStream);

        // コピーした内容をバイト配列に変換して表示
        Console.WriteLine(BitConverter.ToString(toStream.ToArray()));
      }
    }
  }
}

CopyToメソッドでは、コピー元のStreamからReadしたものをコピー先のStreamにWriteします。 この際、コピー元・コピー先ともに現在位置からのコピーが行われます。 つまり現在位置がストリームの先頭でない場合、ストリームの途中からコピーが行われます。 コピー開始時にストリームの先頭へシークされることはありません。 また、CopyToメソッドではコピーする長さを指定することはできず、常にストリームの終端までがコピーされます。


CopyToメソッドでのコピーの際、CopyToメソッド内でデータの読み書きに使用されるバッファが確保されますが、このバッファのサイズを指定することもできます。 なお、指定しなかった場合ではデフォルトで4096バイトのバッファが確保されます。

CopyToメソッドが使用するバッファサイズを指定してStreamをコピーする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream fromStream = File.OpenRead("sample.dat")) {
      using (MemoryStream toStream = new MemoryStream()) {
        // ファイルの内容をMemoryStreamにコピー (バッファサイズとして64[バイト]を指定)
        fromStream.CopyTo(toStream, 64);

        // コピーした内容をバイト配列に変換して表示
        Console.WriteLine(BitConverter.ToString(toStream.ToArray()));
      }
    }
  }
}

CopyToメソッドは.NET Framework 4以降で使用可能なメソッドです。 .NET Framework 3.5以前の場合はCopyToメソッドを使うことは出来ないので、次のようにReadメソッド・Writeメソッドを使ってコピー処理を実装する必要があります。

Read/Writeメソッドを使ってStream.CopyTo相当のメソッドを実装する
using System;
using System.IO;

class Sample {
  static void Copy(Stream fromStream, Stream toStream, int bufferSize)
  {
    // 読み込みに使用するバッファを確保
    byte[] buffer = new byte[bufferSize];

    for (;;) {
      // コピー元のStreamからバッファのサイズ分だけデータを読み込む
      int len = fromStream.Read(buffer, 0, bufferSize);

      if (len == 0)
        // コピー元のStreamの終端まで読み込んだらコピー終了
        break;

      // 読み込んだデータをコピー先のStreamに書き込む
      toStream.Write(buffer, 0, len);
    }
  }

  static void Main()
  {
    using (Stream fromStream = File.OpenRead("sample.dat")) {
      using (MemoryStream toStream = new MemoryStream()) {
        // ファイルの内容をMemoryStreamにコピー (バッファサイズとして64[バイト]を指定)
        Copy(fromStream, toStream, 64);

        // コピーした内容をバイト配列に変換して表示
        Console.WriteLine(BitConverter.ToString(toStream.ToArray()));
      }
    }
  }
}

§3.7 非同期操作

(未整理)

BeginReadBeginWriteなどのメソッドを使うことで、Streamを使って非同期の読み書きを行うことが出来ます。

非同期呼び出しとコールバックについてはデリゲートの機能 §.メソッドの非同期呼び出し (BeginInvoke, EndInvoke)を参照してください。

ストリームの種類によっては操作のタイムアウトもサポートしています。 CanTimeoutプロパティを参照することで操作がタイムアウト可能かどうかを知ることができます。 また、ReadTimeoutプロパティWriteTimeoutプロパティでタイムアウト時間の取得・設定ができます。

複数のスレッドからStreamにアクセスする場合、Synchronizedメソッドメソッドを使うとStreamに対する読み書き操作をスレッドセーフにすることができます。

.NET Framework 4.5からは、ReadAsyncWriteAsyncといった非同期操作のメソッドも使えるようになっています。

§3.8 ヌルオブジェクト (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.Nullを使ってConsole.WriteLineメソッドで出力される内容を破棄する
using System;
using System.IO;

class Sample {
  static void Main(string[] args)
  {
    foreach (string arg in args) {
      // オプションスイッチ/quietが指定された場合は、標準出力へのメッセージ出力を抑止する
      if (arg == "/quiet") {
        // 標準出力の出力先をStream.Nullに設定したStreamWriterに変更する
        Console.SetOut(new StreamWriter(Stream.Null));
        // 次のようにTextWriter.Nullを使うことも可能
        // Console.SetOut(TextWriter.Null);
      }
    }

    // 警告メッセージを標準出力に出力
    Console.WriteLine("warning");

    // 致命的なエラーメッセージを標準エラーに出力
    Console.Error.WriteLine("FATAL ERROR");
  }
}
実行結果
D:\test> Sample.exe
warning
FATAL ERROR

D:\test> Sample.exe /quiet
FATAL ERROR

標準出力のリダイレクトについては自プロセスの標準入出力 §.標準ストリームのリダイレクトで詳しく解説しています。

§3.9 構造体の読み書き

Streamクラスには任意の構造体を読み書きするメソッドは用意されていません。 ストリーム中のブロックを構造体として読み書きしたい場合、fread/fwrite関数のような読み書きを行いたい場合は、いったんバイト配列として読み書きし、さらにバイト配列から構造体に変換する必要があります。 具体的な実装例についてはBinaryReader・BinaryWriterでの構造体の読み書きで紹介しています。