StreamReaderクラスおよびStreamWriterクラスはStreamに対してテキストの読み書きを行うためのクラスです。 Stream単体ではバイナリレベルでの読み書きしか行えませんが、StreamとStreamReader・StreamWriterを組み合わせて使うことでテキストレベルの読み書きが可能になり、ファイルに対して1行ずつ読み書きするといったことができるようになります。

StreamReader・StreamWriterを使ったテキストファイルの読み書き
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtを書き込み用に開く
    using (Stream stream = File.OpenWrite("sample.txt")) {
      // streamに書き込むためのStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream)) {
        // 文字列を改行付きで書き込む
        writer.WriteLine("Hello, world!");
      }
    }

    // ファイルsample.txtを読み込み用に開く
    using (Stream stream = File.OpenRead("sample.txt")) {
      // streamから読み込むためのStreamReaderを作成
      using (StreamReader reader = new StreamReader(stream)) {
        // 文字列を一行読み込んで表示する
        Console.WriteLine(reader.ReadLine());
      }
    }
  }
}

なお、StreamReader・StreamWriterはTextReaderTextWriterから派生したクラスです。 同じくTextReader・TextWriterから派生したクラスにStringReader・StringWriterが存在します。 こちらはStreamのかわりにString・StringBuilderに対してテキストの読み書きを行うためのクラスです。

StreamReaderのメソッドおよびStreamWriterのメソッドはどれもTextReader・TextWriterを継承したもので、そのほとんどはStringReader・StringWriterでも同じように動作します。

§1 導入

StreamReader・StreamWriterの具体的な使い方に入る前に、StreamReader・StreamWriterを扱う上で念頭に置いておくべきことについて解説します。

§1.1 StreamReader・StreamWriterとエンコーディング

StreamReader・StreamWriterでは、Encodingクラスを使った文字列のデコード・エンコードを行う機能が組み込まれています。 コンストラクタに目的のEncodingインスタンスを指定するだけで、UTF-8を始め、Shift_JISやEUC-JPなどの文字コードを深く意識すること無く簡単に扱うことができます。

StreamReader・StreamWriterで使用する文字コードを指定する
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenWrite("sample-utf8.txt")) {
      // UTF-8でエンコードして書き込むためのStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8)) {
        // 文字列はUTF-8にエンコードされた上でstreamに書き込まれる
        writer.WriteLine("こんにちは世界");
      }
    }

    using (Stream stream = File.OpenWrite("sample-shiftjis.txt")) {
      // Shift_JISでエンコードして書き込むためのStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream, Encoding.GetEncoding("Shift_JIS"))) {
        // 文字列はShift_JISにエンコードされた上でstreamに書き込まれる
        writer.WriteLine("こんにちは世界");
      }
    }

    using (Stream stream = File.OpenRead("sample-utf8.txt")) {
      // UTF-8でデコードして読み込むためのStreamReaderを作成
      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) {
        // streamから読み込んだ文字列はUTF-8でデコードされて返される
        Console.WriteLine(reader.ReadLine());
      }
    }
  }
}

Shift_JIS以外にも、Encoding.GetEncodingメソッドにコードページ番号やエンコーディング名を指定することによって、目的の文字コードに対応したEncodingを取得することができます。 具体的な取得方法や、取得できるEncodingについてはファイル入出力 §.Encodingの取得を参照してください。

Encodingを指定しなかった場合には、デフォルトでUTF-8が使用されます。 実行環境のデフォルトのエンコーディングではないので注意してください。 Streamから読み込もうとしているデータのエンコーディングと、StreamReaderに指定するエンコーディングが異なる場合(たとえばShift_JISのファイルを読もうとしてUTF-8を指定したStreamReaderを使った場合)は、当然文字化けが起こるので注意してください。

またStreamWriterでは、Encodingの指定の仕方によってはバイト順マーク(BOM)が書き込まれます。 BOMの扱いについてはSystem.Text.EncodingのBOMありなしの制御で詳しく解説していますので必要に応じて参照してください。 StreamReaderでのBOMの扱いについては、こちらで解説しています

§1.2 読み込み・書き込み対象とStreamReader・StreamWriterの作成方法

§1.2.1 任意のStreamからの読み込み・書き込み

StreamReader・StreamWriterでは読み込み・書き込み対象となるStreamを指定してインスタンスを作成します。 ファイルであればFileStream、バイト配列(メモリ上の領域)であればMemoryStreamなど、データソースとなるStreamを読み込み・書き込み対象として指定します。

FileStreamを読み書きするStreamReader・StreamWriterを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtを書き込み用に開く
    using (FileStream stream = File.OpenWrite("sample.txt")) {
      // streamに書き込むためのStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream)) {
        // 文字列を改行付きで書き込む
        writer.WriteLine("Hello, world!");
      }
    }

    // ファイルsample.txtを読み込み用に開く
    using (FileStream stream = File.OpenRead("sample.txt")) {
      // streamから読み込むためのStreamReaderを作成
      using (StreamReader reader = new StreamReader(stream)) {
        // 文字列を一行読み込んで表示する
        Console.WriteLine(reader.ReadLine());
      }
    }
  }
}
MemoryStreamを読み書きするStreamReader・StreamWriterを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    byte[] data = new byte[32];

    // 既存のバイト配列に書き込むMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(data, true)) {
      // streamに書き込むためのStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream)) {
        // 文字列を改行付きで書き込む
        writer.WriteLine("Hello, world!");
      }
    }

    // 既存のバイト配列から読み込むMemoryStreamを作成
    using (MemoryStream stream = new MemoryStream(data, false)) {
      // streamから読み込むためのStreamReaderを作成
      using (StreamReader reader = new StreamReader(stream)) {
        // 文字列を一行読み込んで表示する
        Console.WriteLine(reader.ReadLine());
      }
    }
  }
}

読み込み・書き込み対象がファイルの場合は、直接ファイル名を指定してStreamReader・StreamWriterインスタンスを作成することもできます。

§1.2.2 ファイルからの読み込み・書き込み

ファイルからの読み書きを行う場合、StreamReader・StreamWriterに直接ファイル名を指定することによりFileStreamインスタンスの作成を省略することが出来ます。 StreamReader・StreamWriterのコンストラクタでファイル名を指定した場合、自動的にFileStreamの作成が行われ、指定されたファイルに対して読み書きを行うStreamReader・StreamWriterを作成することができます。

ファイル名を指定してStreamReader・StreamWriterを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtに書き込むためのStreamWriterを作成
    using (StreamWriter writer = new StreamWriter("sample.txt")) {
      // 文字列を改行付きで書き込む
      writer.WriteLine("Hello, world!");
    }

    // ファイルsample.txtから読み込むためのStreamReaderを作成
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 文字列を一行読み込んで表示する
      Console.WriteLine(reader.ReadLine());
    }
  }
}

後述するFileクラスのOpenText等のメソッドを使うことでもStreamReader・StreamWriterのインスタンスを作成することができます。



§2 StreamReader

ここではStreamReaderの使い方について解説します。 このセクションで紹介するサンプルコードでは、簡単化のためファイル名を指定してStreamReaderを作成していますが、FileStreamやMemoryStreamなどのStreamを指定してStreamReaderのインスタンスを生成した場合も同じように動作します。 また、エンコーディングの指定も省略しています。 サンプル中で使用されるファイルsample.txtは、UTF-8でエンコードされているテキストファイルを想定しています。

§2.1 1行ずつの読み込み (ReadLine)

ReadLineメソッドは、ストリームから1行分の文字列を読み込みます。

ReadLineメソッドを使ってストリームから一行分読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 一行分読み込む
      string line = reader.ReadLine();

      Console.WriteLine("line 1 : {0}", line);

      // 次の行を読み込む
      line = reader.ReadLine();

      Console.WriteLine("line 2 : {0}", line);
    }
  }
}

EndOfStreamプロパティを参照すると、ストリームの末尾まで読み込んだかどうかを調べることができます。 これらを組み合わせてストリームの末尾までを一行ずつ読み込むには次のようにします。

EndOfStreamプロパティをチェックしてストリームの末尾まで一行ずつ読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 行番号
      int lineNumber = 1;

      // 無限ループ
      for (;;) {
        if (reader.EndOfStream)
          // ストリームの末尾(EOF)に達したので読み込みを終了する
          break;

        // 一行分読み込む
        string line = reader.ReadLine();

        // 読み込んだ行に行番号を付けて表示する
        Console.WriteLine("{0}: {1}", lineNumber, line);

        lineNumber++;
      }
    }
  }
}

ReadLineメソッドでは、ストリームの末尾に到達している場合にはnull/Nothingが返されます。 末尾以外の空行(改行文字のみの行)の場合は、null/Nothingではなく長さ0の文字列(String.Empty)が返されます。 ReadLineメソッドの戻り値がnull/Nothingかどうかを調べることでもEOFの判定が行えます。 そのため、上記の例は次のようにも書き換えられます。

ReadLineメソッドの戻り値をチェックしてストリームの末尾まで一行ずつ読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 行番号
      int lineNumber = 1;

      // 無限ループ
      for (;;) {
        // 一行分読み込む
        string line = reader.ReadLine();

        if (line == null)
          // ストリームの末尾(EOF)に達したので読み込みを終了する
          break;

        // 読み込んだ行に行番号を付けて表示する
        Console.WriteLine("{0}: {1}", lineNumber, line);

        lineNumber++;
      }
    }
  }
}

ReadLineメソッドでは、CR+LF、CR(キャリッジリターン)、LF(ラインフィード)の各改行コードまでを1行として扱います。 StreamReaderでは、どの改行コードを行区切りとして扱うかを指定することは出来ません(StreamWriterではStreamWriter.NewLineプロパティで変更可能です)。

また、ReadLineメソッドの戻り値には改行文字は含まれません。 したがって戻り値に対してchomp/chopなどの操作を行う必要はありませんが、一方、改行文字を維持して1行ずつ読み込むことはできません。

参考までに、chomp相当の操作を行うにはString.TrimEndメソッドが使えます。

§2.1.1 File.ReadAllLines, ReadLinesメソッドを使った読み込み

読み込み元がファイルに限定される場合は、行ごとの読み込みにFileクラスのメソッドを使うこともできます。 File.ReadAllLinesメソッドを使うと、ファイルを読み込み行ごとに分割したものを文字列配列として取得できます。 次の例では、File.ReadAllLinesでファイルを読み込み行番号を付けて表示しています。 比較のためにStreamReader.ReadLineを使って同等の処理を行う例も併記しています。

File.ReadAllLinesとStreamReader.ReadLineを使った行ごとの読み込みの比較
using System;
using System.Collections.Generic;
using System.IO;

class Sample {
  static void Main()
  {
    // File.ReadAllLinesでファイル全体を読み込んだ後、行番号を付けて表示する
    string[] lineArray = File.ReadAllLines("sample.txt");

    for (int index = 0; index < lineArray.Length; index++) {
      Console.WriteLine("{0}: {1}", index + 1, lineArray[index]);
    }

    // StreamReaderでファイル全体を読み込んだ後、行番号を付けて表示する
    List<string> lineList = new List<string>();

    using (StreamReader reader = new StreamReader("sample.txt")) {
      for (string line = reader.ReadLine(); line != null; line = reader.ReadLine()) {
        lineList.Add(line);
      }
    }

    for (int index = 0; index < lineList.Count; index++) {
      Console.WriteLine("{0}: {1}", index + 1, lineList[index]);
    }
  }
}

.NET Framework 4からはFile.ReadLinesメソッドも使うことができるようになっています。 File.ReadAllLinesはファイル全体の読み込みが終わるまで結果が返されないのに対し、File.ReadLinesでは一行読み込むごとに結果が返されます。 巨大なファイルを読み込む場合や、ファイルの先頭部分のみを読み込みたい場合などではReadAllLinesメソッドを使うよりもReadLinesメソッドを使った方が効率的です。

File.ReadLinesとStreamReader.ReadLineを使った行ごとの読み込みの比較
using System;
using System.Collections.Generic;
using System.IO;

class Sample {
  static void Main()
  {
    // File.ReadLinesでファイルを1行ずつ読み込み、行番号を付けて表示する
    int lineNumber = 1;

    foreach (string line in File.ReadLines("sample.txt")) {
      Console.WriteLine("{0}: {1}", lineNumber, line);

      lineNumber++;
    }

    // StreamReaderでファイルを1行ずつ読み込み、行番号を付けて表示する
    lineNumber = 1;

    using (StreamReader reader = new StreamReader("sample.txt")) {
      foreach (string line in ReadLines(reader)) {
        Console.WriteLine("{0}: {1}", lineNumber, line);

        lineNumber++;
      }
    }
  }

  // File.ReadLinesに相当する処理をStreamReaderに対して行えるようにするメソッド
  private static IEnumerable<string> ReadLines(StreamReader reader)
  {
    for (;;) {
      string line = reader.ReadLine();

      if (line == null)
        yield break;
      else
        yield return line;
    }
  }
}

この例で使用しているイテレータ構文についてはイテレータを参照してください。

Fileクラスのメソッドを使ったファイルの読み込みについてはファイル入出力を参照してください。

§2.2 全テキストの読み込み (ReadToEnd)

ReadToEndメソッドは、ストリームの現在位置から末尾までをひとつながりの文字列として読み込みます。

ReadToEndメソッドを使ってストリームの末尾までの全テキストをひとつの文字列として読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // ストリームの末尾まですべてを読み込む
      string content = reader.ReadToEnd();

      Console.WriteLine(content);
    }
  }
}

§2.2.1 File.ReadAllTextを使った読み込み

読み込み元がファイルに限定される場合はFileクラスのメソッドを使うこともできます。 File.ReadAllTextメソッドを使うと、StreamReaderを使わずにファイルの内容を文字列として読み込むことができます。

File.ReadAllTextとStreamReader.ReadToEndを使ったファイル全体の読み込みの比較
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // File.ReadAllTextでファイル全体を読み込む
    string content = File.ReadAllText("sample.txt");

    Console.WriteLine(content);

    // StreamReader.ReadToEndでファイル全体を読み込む
    using (StreamReader reader = new StreamReader("sample.txt")) {
      content = reader.ReadToEnd();

      Console.WriteLine(content);
    }
  }
}

Fileクラスのメソッドを使ったファイルの読み込みについてはファイル入出力を参照してください。

§2.3 文字単位での読み込み (Read, Peek)

Readメソッドは、ストリームから1文字ずつ読み込みます。 Readメソッドで読み込まれるのは1バイトずつではありません。 戻り値はint/Integerで、ストリームの末尾に達した場合は-1が返されます。 それ以外の場合は読み込めた文字(char)の値となるため、戻り値をchar/Charにキャストして使います。

Readメソッドを使ってストリームから一文字ずつ読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 無限ループ
      for (;;) {
        // 一文字読み込む
        int ch = reader.Read();

        if (ch == -1)
          // ストリームの末尾(EOF)に達したので読み込みを終了する
          break;

        // 読み込んだ値をcharにキャストして表示する
        char c = (char)ch;

        Console.Write(c);
      }
    }
  }
}

Peekメソッドを使うと、次の1文字を先読みすることができます。 Peekメソッドの戻り値はReadメソッドの戻り値と同様です。

次の例では、ReadメソッドとPeekメソッドを使って読み込みを行い、各行に行番号を付けて表示しています。 この例ではCR, LF, CR+LFを改行として扱い、それらの改行文字は変換せずそのまま表示しています。

Peekメソッドを使ってストリームの次の一文字を先読みする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      int lineNumber = 1;

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

      // 無限ループ
      for (;;) {
        // 一文字読み込む
        int ch = reader.Read();

        if (ch == -1)
          // ストリームの末尾(EOF)に達したので読み込みを終了する
          break;

        // 値をcharにキャストして表示
        char c = (char)ch;

        Console.Write(c);

        // 読み込んだ文字がCRだった場合
        if (c == '\r') {
          // 次の一文字を読み込む
          if (reader.Peek() == (int)'\n')
            // 現在のCRにLFが後続するので、ここでは何もせず処理を続ける
            // (次のループでLFが読まれる)
            continue;
          else
            // CR単独の改行なので、行番号を表示する
            Console.Write("{0}: ", ++lineNumber);
        }
        // 読み込んだ文字がLFだった場合
        else if (c == '\n') {
          // LF単独もしくはCR+LFの改行なので、行番号を表示する
          Console.Write("{0}: ", ++lineNumber);
        }
      }
    }
  }
}

引数を指定せずにReadメソッドを呼び出した場合は1文字ずつの読み込みが行われますが、Readメソッドでは読み込み先のバッファと文字数を指定して読み込むこともできるようになっています。 文字を1文字ずつチェックする必要がある場合などでは、Readメソッドで1文字ずつ読み込んでチェックするよりも、バッファに複数文字読み込んでチェックした方がメソッド呼び出しの回数が少なくなり効率的です。

次の例では、Readメソッドで一旦バッファに読み込んでから1文字ずつチェックして全角英字を半角英字に置き換えています。

Readメソッドを使ってバッファと読み込む文字数を指定して読み込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamReader reader = new StreamReader("sample.txt")) {
      // 読み込んだ文字を格納しておくためのバッファ (32文字分)
      char[] buffer = new char[32];

      for (;;) {
        // StreamReaderから読み込んでバッファに格納する
        int len = reader.Read(buffer, 0, buffer.Length);

        if (len == 0)
          // ストリームの末尾(EOF)に達したので読み込みを終了する
          break;

        // 読み込んだ文字を1文字ずつ検査して全角英字を半角英字に変換する
        for (int i = 0; i < len; i++) {
          if ('A' <= buffer[i] && buffer[i] <= 'Z')
            buffer[i] = (char)((int)buffer[i] - (int)'A' + (int)'A');
          else if ('a' <= buffer[i] && buffer[i] <= 'z')
            buffer[i] = (char)((int)buffer[i] - (int)'a' + (int)'a');
        }

        // bufferを文字列に変換して表示
        Console.Write(new string(buffer, 0, len));
      }
    }
  }
}
sample.txtの内容
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
0123456789
あいうアイウ漢字
出力結果
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789
0123456789
あいうアイウ漢字

§2.4 ブロッキングを行う読み込み (ReadBlock)

ReadBlockメソッドは、読み込み先のバッファと文字数を指定してReadメソッドを呼び出した場合と基本的には同じです。

ReadメソッドとReadBlockメソッドを使った読み込みの違い
using System;
using System.IO;

class Sample {
  static void Main()
  {
    char[] buffer = new char[32];

    using (StreamReader reader = new StreamReader("sample.txt")) {
      // Readメソッドを使ってストリームの末尾に達するまで読み込みを行い、内容を表示する
      for (;;) {
        int len = reader.Read(buffer, 0, buffer.Length);

        if (len == 0)
          break;

        Console.Write(new string(buffer, 0, len));
      }
    }

    using (StreamReader reader = new StreamReader("sample.txt")) {
      // ReadBlockメソッドを使ってストリームの末尾に達するまで読み込みを行い、内容を表示する
      for (;;) {
        int len = reader.ReadBlock(buffer, 0, buffer.Length);
        
        if (len == 0)
          break;

        Console.Write(new string(buffer, 0, len));
      }
    }
  }
}

ReadメソッドとReadBlockメソッドの違いは、読み込みに際してブロッキングが行われるかどうかという点にあります。 Readメソッドではブロッキングされませんが、ReadBlockメソッドではブロッキングされます。

ReadBlockメソッドは、ストリームの末尾に到達するか、指定された文字数の読み込みが終わるまで処理を返しません。 一方Readメソッドは、ストリームの末尾に到達した場合のほか、指定された文字数よりも少ない文字数しか読み込めなかった場合でも処理を返します。 これはNetworkStreamを使った読み込み中にデータの途中までを受信した場合などに起こる可能性があります(FileStreamやMemoryStreamでは通常起こらないと思われます)。 従って、データの途中でも読み込みができた時点で結果を返させるようにしたければReadメソッド、指定した文字数を読み込めた時点で結果を返させるようにしたい場合にはReadBlockメソッドを使います。

こういった動作の違いについては、Stream.Readメソッドの動作についてもあわせてご覧ください。

§2.5 StreamReaderとBOMの扱い

先頭にBOMの付いたストリームをStreamReaderで読み込む場合、ReadToEndやReadLineメソッドからはBOMが除去された文字列が返されます。 この動作はエンコーディングを指定した場合・省略した場合のどちらでも同じです。 ReadToEnd・ReadLine以外のメソッドでも同様にBOMの除去された文字列が返されます。

StreamReaderでBOM付きのストリームを読み込んだ場合
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // UTF-8でエンコードされた文字列をBOM付きでファイルに書き込む
    File.WriteAllBytes("sample.txt", new byte[] {0xEF, 0xBB, 0xBF, 0xE6, 0x97, 0xA5, 0xE6, 0x9C, 0xAC, 0xE8, 0xAA, 0x9E}); // [BOM] + '日本語'

    using (var reader = new StreamReader("sample.txt")) {
      // encodingを指定せずに作成したStreamReaderのReadToEndで読み込む
      Print(reader.ReadToEnd());
    }

    using (var reader = new StreamReader("sample.txt")) {
      // encodingを指定せずに作成したStreamReaderのReadLineで読み込む
      Print(reader.ReadLine());
    }

    using (var reader = new StreamReader("sample.txt", Encoding.UTF8)) {
      // encodingを指定して作成したStreamReaderのReadToEndで読み込む
      Print(reader.ReadToEnd());
    }

    using (var reader = new StreamReader("sample.txt", Encoding.UTF8)) {
      // encodingを指定して作成したStreamReaderのReadLineで読み込む
      Print(reader.ReadLine());
    }
  }
  
  static void Print(string text)
  {
    Console.Write("Length = {0} : {1} ", text.Length, text);

    foreach (var c in text) {
      Console.Write("U+{0:X} ", (int)c);
    }

    Console.WriteLine();
  }
}
出力結果
Length = 3 : 日本語 U+65E5 U+672C U+8A9E 
Length = 3 : 日本語 U+65E5 U+672C U+8A9E 
Length = 3 : 日本語 U+65E5 U+672C U+8A9E 
Length = 3 : 日本語 U+65E5 U+672C U+8A9E 

BOMを含めたまま読み込みを行いたい場合は、Streamから直接読み込むか、BinaryReaderクラスを使って読み込み処理を記述する必要があります。

§3 StreamWriter

ここではStreamWriterの使い方について解説します。 このセクションで紹介するサンプルコードでは、簡単化のためファイル名を指定してStreamWriterを作成していますが、FileStreamやMemoryStreamなどのStreamを指定してStreamWriterのインスタンスを生成した場合も同じように動作します。 また、エンコーディングの指定も省略しています。

§3.1 改行付きの書き込み (WriteLine)

WriteLineメソッドは、ストリームに1行分の文字列を書き込みます。

WriteLineメソッドを使ってストリームに一行分書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamWriter writer = new StreamWriter("sample.txt")) {
      // 1行目を書き込む
      writer.WriteLine("line 1");

      // 2行目を書き込む
      writer.WriteLine("line 2");

      // 3行目を書き込む
      writer.WriteLine("line 3");
    }
  }
}

WriteLineメソッドでは、書き込みの際に改行文字が付け加えられます。 デフォルトでは改行文字としてプラットフォームの改行文字が使われますが、NewLineプロパティに値を設定することで使用する改行文字を変更することができます。

また、WriteLineメソッドに文字列以外を引数に指定した場合は、文字列に変換した上で書き込まれます。 複合書式設定を使用することで書式を指定したり0埋め・右詰め・左詰めした上で書き込むこともできます。

WriteLineメソッドを使って書式化した値をストリームに書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamWriter writer = new StreamWriter("sample.txt")) {
      // 数値を書き込む
      writer.WriteLine(16);
      writer.WriteLine(Math.PI);

      // 現在の日付を書き込む
      writer.WriteLine(DateTime.Now);

      // 複数の値を一行に書き込む
      writer.WriteLine("{0} {1} {2:hh:mm}", 42, 72, DateTime.Now);

      // 書式を指定して値を書き込む
      writer.WriteLine("|{0,10:D6}|", 12345);  // 数値を幅10・6桁に0埋めして右揃え
      writer.WriteLine("|{0,-10:D6}|", 12345); // 数値を幅10・6桁に0埋めして左揃え
    }
  }
}

文字列への変換に際して、現在のスレッドのカルチャが書式プロバイダとして使用されます。 これはFormatProviderプロパティで参照可能です。 書式指定子や書式プロバイダについての詳細は以下のページをご覧ください。

§3.2 改行無しの書き込み (Write)

Writeメソッドは、WriteLineメソッドとは異なり書き込みの際に改行文字が追記されません。 それ以外はWriteLineメソッドと同じです。

Writeメソッドを使ってストリームに改行なしで文字列を書き込む
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamWriter writer = new StreamWriter("sample.txt")) {
      // 数値を書き込む
      writer.Write(16);
      writer.Write(" ");
      writer.Write(Math.PI);

      // 改行文字を書き込む
      writer.Write(Environment.NewLine);

      // 複数の値を一行に書き込む
      writer.Write("{0} {1} {2:hh:mm}", 42, 72, DateTime.Now);

      // 改行文字を書き込む
      writer.Write(Environment.NewLine);
    }
  }
}

書き込む改行文字や改行の位置を細かく指定したい場合にはWriteメソッドを使うことができます。

§3.3 改行文字

StreamWriterのWriteLineメソッドは、デフォルトではプラットフォームの改行文字を使用します(ランタイム・システム・プラットフォームの情報 §.改行文字)。 NewLineプロパティに値を設定することでStreamWriterが使用する改行文字を変更することができます。 NewLineプロパティではCR(\r)やLF(\n)以外の文字を含めたり、長さ0の文字列を設定することもできます。

StreamWriterがWriteLineメソッドで書き込む改行文字を変更する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (StreamWriter writer = new StreamWriter("sample.txt")) {
      writer.WriteLine("line1");
      writer.WriteLine("line2");

      // 改行文字を変更する
      // 改行を表す記号にプラットフォームの改行文字を付けたものを指定する
      writer.NewLine = "↵" + Environment.NewLine;

      writer.WriteLine("line3");
      writer.WriteLine("line4");
    }
  }
}
出力されるsample.txtの内容
line1
line2
line3↵
line4↵

§3.4 ファイルへの追記

StreamWriterのコンストラクタでファイル名を指定する場合、同時にファイルを追記モードで開くかどうかを指定することができるようになっています。 コンストラクタの引数appendにtrueを指定すれば、ファイルの末尾から書き込みを開始するようにしたStreamWriterインスタンスが作成されます。 省略した場合やfalseを指定した場合は上書きモードでファイルが開かれ、既存のファイルを開く場合にはその内容が破棄されます。

上書きモード・追記モードでファイルを開いたStreamWriterを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 上書きモードでsample.txtを開く
    using (StreamWriter writer = new StreamWriter("sample.txt", false)) {
      // 以下の内容はsample.txtに上書きされる
      writer.WriteLine("line1");
      writer.WriteLine("line2");
    }

    // 追記モードでsample.txtを開く
    using (StreamWriter writer = new StreamWriter("sample.txt", true)) {
      // 以下の内容はsample.txtに追記される
      writer.WriteLine("line3");
      writer.WriteLine("line4");
    }
  }
}
出力されるsample.txtの内容
line1
line2
line3
line4

§3.5 Fileクラスを使った書き込み

書き込み先がファイルで、書き込む内容がすでに文字列として存在している場合は、Fileクラスのメソッドを使って書き込むこともできます。 File.WriteAllTextメソッドを使うと、指定した文字列を指定したファイルに書き込むことができます。 このメソッドを使うことでStreamWriterを作成せずにファイルへの書き込みが行えるため、書き込み処理の記述を簡略化することができます。

File.WriteAllTextメソッドを使ってファイルに文字列を書き込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // ファイルに書き込む内容
    string content = @"line1
line2
line3
line4
";

    // 文字列contentの内容をUTF-8でエンコードしてsample.txtに書き込む
    File.WriteAllText("sample.txt", content, Encoding.UTF8);
  }
}
出力されるsample.txtの内容
line1
line2
line3
line4

File.WriteAllTextメソッドでは既存のファイルに対する書き込みの場合、ファイルの内容は上書きされますが、File.AppendAllTextメソッドを使うと既存のファイルの内容に追記することができます。

File.AppendAllTextメソッドを使ってファイルに文字列を追記する
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // ファイルに上書きする内容
    string content = @"line1
line2
";

    // 文字列contentの内容をsample.txtに上書き
    File.WriteAllText("sample.txt", content, Encoding.UTF8);

    // ファイルに追記する内容
    content = @"line3
line4
";
    // 文字列contentの内容をsample.txtに追記
    File.AppendAllText("sample.txt", content, Encoding.UTF8);
  }
}
出力されるsample.txtの内容
line1
line2
line3
line4

File.WriteAllLinesメソッドを使うと、文字列配列の各要素を1行としてファイルに書き込むことができます。 File.WriteAllLinesメソッドでは、書き込みの際に自動的に改行文字が付加されます。

File.WriteAllLinesメソッドを使ってファイルに行ごとの内容を書き込む
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // ファイルに書き込む内容
    string[] lines = new string[] {
      "line1",
      "line2",
      "line3",
      "line4",
    };

    // 配列linesの各要素を1行としてsample.txtに書き込む
    File.WriteAllLines("sample.txt", lines, Encoding.UTF8);
  }
}
出力されるsample.txtの内容
line1
line2
line3
line4

.NET Framework 4からは文字列配列だけでなく任意のIEnumerable<string>を指定することもできるようになっています。 そのため、書き込みたい内容がList<string>に格納されている場合でも配列に変換する必要はなくなりました。 また、.NET Framework 4からはFile.AppendAllLinesメソッドも追加されていて、このメソッドを使うことで文字列配列をファイルに追記することができます。

File.WriteAllLines・AppendAllLinesにIEnumerable<string>を指定して書き込む
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    // ファイルに上書きする内容
    List<string> lines = new List<string>();

    lines.Add("line1");
    lines.Add("line2");

    // linesの内容をsample.txtに上書き
    File.WriteAllLines("sample.txt", lines, Encoding.UTF8);

    // ファイルに追記する内容
    lines.Clear();
    lines.Add("line3");
    lines.Add("line4");

    // linesの内容をsample.txtに追記
    File.AppendAllLines("sample.txt", lines, Encoding.UTF8);
  }
}
出力されるsample.txtの内容
line1
line2
line3
line4

Fileクラスのメソッドを使ったファイルの書き込みについてはファイル入出力を参照してください。

§4 Fileクラスのメソッドを使ったインスタンスの作成

Fileクラスのメソッドを使ってFileStreamを作成できるのと同様、Fileクラスには指定したファイルを開いてStreamReader・StreamWriterを作成するためのメソッドがいくつか用意されています。

File.OpenTextメソッドを使うと、ファイル名を指定するだけでStreamReaderを作成することができます。 このメソッドを使うと、FileStreamインスタンスの作成を省略してStreamReaderを作成することができます。

File.OpenTextメソッドを使ってファイルを読み込むStreamReaderを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtを読み込むStreamReaderを作成する
    using (StreamReader reader = File.OpenText("sample.txt")) {
      Console.WriteLine(reader.ReadLine());
    }
  }
}

同様に、File.CreateTextメソッドを使うと、ファイル名を指定するだけでStreamWriterを作成することができます。 また、File.AppendTextメソッドを使えば、ファイルへの追記を行うStreamWriterを作成できます。

File.CreateText・AppendTextメソッドを使ってファイルに書き込む・追記するStreamWriterを作成する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtに書き込むStreamWriterを作成する
    using (StreamWriter writer = File.CreateText("sample.txt")) {
      writer.WriteLine("line1");
      writer.WriteLine("line2");
    }

    // ファイルsample.txtに追記を行うStreamWriterを作成する
    using (StreamWriter writer = File.AppendText("sample.txt")) {
      writer.WriteLine("line3");
      writer.WriteLine("line4");
    }
  }
}

なお、これらのメソッドではエンコーディングを指定することはできないので、UTF-8以外で読み書きを行いたい場合はStreamReader・StreamWriterのコンストラクタを使用してインスタンスを作成する必要があります。

§5 StreamReader・StreamWriterとベースとなるストリームのクローズ

StreamReader・StreamWriterでは、Closeメソッドを呼び出したりusingステートメントから抜けた際、ベースとなったストリームも閉じられ、ストリームに対するアクセスができなくなります。

StreamReader・StreamWriterのクローズと、ベースとなったストリームへのアクセス
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.txt")) {
      // streamをベースにしてStreamReaderを作成
      using (StreamReader reader = new StreamReader(stream)) {
        reader.ReadLine();
      }
      // usingステートメントから抜ける時点でreader.Disposeメソッドが呼び出される
      // その際、readerが使用していたstreamもCloseされるため、その後streamのメソッドを
      // 呼び出そうとするとObjectDisposedExceptionがスローされる
      stream.ReadByte();
    }

    using (Stream stream = File.OpenWrite("sample.txt")) {
      // streamをベースにしてStreamWriterを作成
      using (StreamWriter writer = new StreamWriter(stream)) {
        writer.WriteLine("text");
      }

      // StreamWriterの場合も同様に使用していたstreamはCloseされるため、
      // ObjectDisposedExceptionがスローされる
      stream.WriteByte(0);
    }
  }
}

StreamReader・StreamWriterを使用したあとも引き続きベースとなったストリームを使い続けたい場合は、usingステートメントは使わず、使用したStreamReader・StreamWriterを閉じないようにします。

ただ、StreamWriterでは閉じる際に同時にフラッシュも行われるようになっているため、usingステートメントやCloseメソッドでStreamWriterを閉じない場合は、StreamWriterを使い終わった時点でFlushメソッドを呼び出し、書き込んだ内容をフラッシュさせるようにする必要があります。

StreamReader・StreamWriterを閉じないようにして使用後もベースとなったストリームへアクセスできるようにする
using System;
using System.IO;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.txt")) {
      // usingステートメントを使わずにStreamReaderを作成・使用する
      StreamReader reader = new StreamReader(stream);

      reader.ReadLine();

      // StreamReaderを使用した後に、streamを直接使用する
      stream.ReadByte();
    }

    using (Stream stream = File.OpenWrite("sample.txt")) {
      // usingステートメントを使わずにStreamWriterを作成・使用する
      StreamWriter writer = new StreamWriter(stream);

      writer.WriteLine("text");

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

      // StreamWriterを使用した後に、streamを直接使用する
      stream.WriteByte(0);
    }
  }
}

.NET Framework 4.5以降では、コンストラクタの引数leaveOpenにtrueを指定すると、StreamReader・StreamWriterを閉じてもベースとなるストリームを開いたままにすることができます。 ただし、leaveOpenを指定する場合は、同時に引数encodingdetectEncodingFromByteOrderMarksおよびbufferSizeも省略せずに指定する必要があります。

コンストラクタの引数leaveOpenを指定してStreamReader・StreamWriterを閉じてもベースとなるストリームを開いたままにする
using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    using (Stream stream = File.OpenRead("sample.txt")) {
      // leaveOpenにtrueを指定してStreamReaderを作成
      const bool leaveOpen = true;

      using (StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, 1024, leaveOpen)) {
        reader.ReadLine();
      }

      // usingステートメントから抜けてreaderが閉じられても
      // streamは閉じられないのでObjectDisposedExceptionはスローされない
      stream.ReadByte();
    }

    using (Stream stream = File.OpenWrite("sample.txt")) {
      // leaveOpenにtrueを指定してStreamWriterを作成
      const bool leaveOpen = true;

      using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8, 1024, leaveOpen)) {
        writer.WriteLine("text");
      }

      // StreamWriterの場合も同様で、ObjectDisposedExceptionはスローされない
      stream.WriteByte(0);
    }
  }
}

ベースとなるストリームの扱いについてはBinaryReader・BinaryWriterでも同様で、.NET Framework 4.5以降ではBinaryReader・BinaryWriterにおいても使用するストリームを開いたままにするかどうかをコンストラクタで指定できるようになっています。

直接ファイル名を指定してインスタンスを作成した場合や、File.OpenTextなどのメソッドを使ってインスタンスを作成した場合は、かならず作成したStreamReader/StreamWriterを閉じるようにしなければなりません。 これらのメソッドではFileStreamが内部的に作成されますが、StreamReader/StreamWriterから閉じないかぎり内部的に作成されたFileStreamは閉じられません。 従って、StreamReader/StreamWriterによって閉じるまではファイルは開かれたままとなるため、この間他からファイルへのアクセスを行うことができなくなります。

直接ファイル名を指定して作成したStreamWriter/StreamReaderを閉じなかった場合
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // ファイルsample.txtに書き込むためのStreamWriterを作成
    StreamWriter writer = new StreamWriter("sample.txt");

    writer.WriteLine("Hello, world!");

    // usingステートメントを使わず、Closeメソッドも呼び出さない場合、
    // sample.txtを開いているStreamWriter内部のFileStreamは開かれたままとなる

    //writer.Close();

    // ファイルsample.txtから読み込むためのStreamReaderを作成
    // (sample.txtは上記のStreamWriterが開いたままのため、
    //  StreamReaderは開くことができずIOExceptionがスローされる)
    StreamReader reader = new StreamReader("sample.txt");

    Console.WriteLine(reader.ReadLine());
  }
}