C#プログラミングTips

ファイル入出力の基礎

デジタルアドバンテージ
2001/05/30
2002/03/07 更新


 ファイル入出力は、アプリケーションの種類とは無関係にほとんどのプログラムにおいて必須の処理である。ここでは、.NET Frameworkにおけるファイル入出力の基礎について解説する。

ストリームとは

 .NET Frameworkで提供されるファイル入出力処理は、「ストリーム(Stream)」という概念に基づく。ストリームを簡単に言えば、「ストレージからの、あるいはストレージへのデータの流れ」である。今回のテーマである「ファイル」は、このストレージの1つである。しかし.NET Frameworkでは、ファイルだけでなく、メモリやネットワークも、ファイルと同様にストレージとして、つまりはストリームの源として扱うことができる。

 .NET Frameworkにおいて、ストリームを定義しているクラスはStreamクラスである。Streamクラスは、型のないバイトの集まりとしてストリームを読み書きする操作を定義した抽象クラスだ。実際にストリームを扱うクラスはすべてStreamクラスから派生したクラスで実装されている。下の図はStreamクラスとStreamクラスをベース・クラスとする代表的な派生クラスの階層を図示したものだ。

抽象クラスであるStreamクラスと、その代表的な派生クラス

 これらの派生クラスのなかでも、BufferedStreamクラスは、他のクラスとは少々毛色が異なる。このクラスは、それ自身が特定のストレージを対象としたストリームではなく、他のストリームに対する入出力をバッファリングするためのストリームである。バッファリングによって、低速なストレージに対するアクセス回数を減らし、入出力性能を向上させることを目的としている。ただしFileSteamクラスは内部でバッファリングを行っており、またMemoryStreamクラスではバッファリングを行う必要がないため、これらのストリームに対しては不要である。主にNetworkStreamクラスと組み合わせて使用するようだ。

 例えばStreamクラスには、ストリームから1byte読み込むためのメソッドであるReadByte( )が抽象メソッドとして定義されている。このためすべての派生クラスでは、ReadByte()メソッドが実装されている。つまりプログラムでは、対象となるストリームがファイルであっても、ネットワークであっても、まったく同じようにReadByte( )により1byteのデータを取得できるわけだ。

サンプル・プログラム c#p

 今回はまず、FileStreamクラスを使用して、ファイルをコピーするコマンド「c#p.exe」を作ってみる(これはUNIXのコピー・コマンドである「cp」のC#バージョンだ)。コマンド・プロンプトから、

C:\> c#p コピー元のファイル名 コピー先のファイル名

と指定すれば、ファイルが別の新しいファイルにコピーされる。c#p.exeのソース・コードは次のとおりである。

 1: // c#p.cs
 2:
 3: using System;
 4: using System.IO;
 5:
 6: public class CpSample {
 7:   public static void Main(string[] args) {
 8:
 9:     int b;
10:     FileStream infs  = new FileStream(args[0], FileMode.Open);
11:     FileStream outfs = new FileStream(args[1], FileMode.Create);
12:
13:     while ((b = infs.ReadByte()) != -1) {
14:       outfs.WriteByte((byte)b);
15:     }
16:     infs.Close();
17:     outfs.Close();
18:   }
19: }
C#版cpコマンドであるc#pのソース・コード

コマンドラインの引数

 FileStreamクラスについて説明する前に、コマンドラインで指定された引数をプログラムで取得する方法について簡単に説明しておこう。例えば、今回のc#pプログラムでは、コピー元とコピー先のファイル名がコマンドラインの引数として与えられる。

 コマンドラインに指定された引数をプログラムで使用する場合には、Mainメソッドの引数として、文字列型の配列を表す変数を1つ記述する。この際、変数名は何でもかわまないが、たいていは7行目のように「args」とすることが多いようだ。コマンドラインの引数を取得する必要がなければ、Mainメソッドの引数は省略してよい。なお、リストの7行目にあるとおり、Mainメソッドの戻り値はvoid型(戻り値を返さないとき)かint型(戻り値を返すとき)のどちらかでなければならない。

7: public static void Main(string[] args) {
プログラムでコマンドラインのパラメータを取得する場合のMainメソッドの定義

 この場合、コマンドラインの引数として指定された文字列は、前から順に(コマンドラインの左から順に)args[0]args[1]、…というふうにアクセスすることができる。C/C++と違い、最初のパラメータのインデックスは0である。argsは配列なので、指定されたパラメータの総数を知りたければargs.Lengthで取得できる。これを利用すると、最後のパラメータはargs[args.Length - 1]となる。

FileStreamクラス

 c#pプログラムでは、コピー元とコピー先の2つのファイルを扱うため、2つのFileStreamオブジェクトが必要になる。クラス・ライブラリのリファレンス・マニュアルを参照すると、FileStreamクラスには引数の異なる何種類ものコンストラクタが用意されているが、今回は基礎と言うことで、次のように2つのパラメータを持つ最も簡単なコンストラクタを使用した。こうしてファイルに対するFileStreamオブジェクトを作成すると、プログラムではそのファイルを開いたことになり、ファイルへの入出力が可能になる。

10: FileStream infs  = new FileStream(args[0], FileMode.Open);
11: FileStream outfs = new FileStream(args[1], FileMode.Create);
引数にファイルのパスとファイル・モードを指定して、FileStreamオブジェクトを作成する

 このコンストラクタの引数は、ファイルのパス名と、ファイルを開くときのファイル・モードである。10行目はコピー元のファイル・ストリームを作成するもので、コピー元ファイルは既存ファイルで、プログラムではこれを読み込むだけなので、ファイル・モードとしてFileMode.Open(既存ファイルのオープン)を指定する。一方、11行目はコピー先のファイルに対するファイル・ストリームである。こちらは新しくファイルを作成しなければならないため、ファイル・モードとしてFileMode.Create(ファイルを新規に作成してオープン)を指定する。FileMode.Createモードでは、同一ファイル名のファイルが存在する場合には、そのファイルを上書きモードでオープンする。

ReadByteメソッドとWriteByteメソッド

 FileSteramオブジェクトを作ってしまえば、次はコピー元のストリームからデータを読みつつ、そのデータをコピー先のストリームに書き出すだけだ。ここではストリームから1byteを順に読み込むReadByteメソッドと、ストリームに1byteのデータを順に書き込むWriteByteメソッドを組み合わせてコピー処理を行った。ReadByteメソッドはストリームの最後まで読み終わると-1を返すため、その時点でコピーをやめればよい。

 9: int b;
       ・・・
13: while ((b = infs.ReadByte()) != -1) {
14:   outfs.WriteByte((byte)b);
15: }
ファイルの終端に達するまで、ReadByteメソッドによりコピー元から1byteずつ読み込み、WriteByteメソッドによってコピー先に書き込む

 プログラムを簡単にするために、今回は1byteずつ読み書きを行ったが、FileStreamクラスには複数バイトをまとめて読み書きできるReadメソッドとWriteメソッドが用意されている。より効率的にコピーしたければ、こちらの組み合わせを使うべきだろう。

 ストリーム・オブジェクトを作成して開いたファイルは、処理が終わったらCloseメソッドにより閉じる必要がある。特に書き込みを行ったストリームは高速化のためにメモリにバッファリングされているため、Closeメソッドを呼び出さないと、ファイルへの書き出し(ディスクへの書き込み)が完了しない可能性がある。ただしFileStreamクラスのFlushメソッドを呼び出せば、ファイルを閉じなくても、書き込み処理中に随時バッファリングされたデータをファイルに書き出すことができる。

16: infs.Close();
17: outfs.Close();
ストリームへの読み書きがすべて完了したらCloseメソッドによりストリームを閉じる

TextReaderクラスとTextWriterクラス

 前出のStreamクラスでは、ストリームをバイト・データの列として、その中身が何であるかをまったく意識することなく、データの読み書きを行っていた。しかし現実に多くのアプリケーションでは、データの中身を意識しながら、プログラムで必要に応じて調整処理を行う必要に迫られる。典型的な例は、テキスト・ファイルの読み書きである。周知のとおり日本語では、Shift-JISコードやUnicodeなど、1文字を複数バイトで表現する多バイト文字コードを使用する。1文字が常に1byteだと保証されているなら問題はないのだが、Shift-JISやUnicodeなどの多バイト文字では、処理によって文字を表すデータが分断されてしまわないようにプログラムで調整しなければならない。幸いなことに.NET Frameworkには、これらの面倒を見てくれるクラスが用意されている。これがTextReaderクラスとTextWriterクラスである。

 TextReaderクラスとTextWriterクラスは、ともに文字としてストリームを扱う抽象クラスである。先ほどのStreamクラスと同様にクラス階層を図示すると次のようになる。

TextReader、TextWriter抽象クラスと、その派生クラス

 このようにTextReaderクラスとTextWriterクラスはおのおの2つの派生クラスを持っているが、今回注目するのはStreamReader/StreamWriterクラスだ。この2つのクラスは、byteデータを扱うストリーム・オブジェクトをラップするクラスで、ストリームの種類(ファイル・ストリームか、ネットワーク・ストリームかなど)にかかわらず文字単位の読み書きを行うRead/Writeメソッドや、行単位の読み書きを行うReadLine/WriteLineメソッドが提供される。またこれらのクラスでは、そのコンストラクタでストリームに対するエンコーディング方法を指定することで、さまざまなコード体系のテキストを扱えるようにしている。具体的には、もう1つサンプル・プログラムを作って説明することにしよう。


サンプル・プログラム c#at

 StreamReaderクラスを使用したサンプル・プログラムとして、UNIXのcatコマンドのC#バージョン「c#at」を作成する。UNIX環境では、このcatコマンドをMS-DOSのtypeコマンドのように使って、ファイルの内容を表示するためによく利用されるが、catという名前の由来である「concatenate」(「連結する」の意)が表すように、本来はテキスト・ファイル同士を連結するためのコマンドだ(catコマンドは処理結果を標準出力に出力するため、ファイルの内容を画面に表示することができる)。c#atプログラムも、コマンド・プロンプトから次のように実行することで、複数のテキスト・ファイルを1つに連結し、それらを標準出力である画面に表示する。

C:\> c#at ファイル1 ファイル2 ファイル3・・・

 連結されたファイルをファイルとして保存したければ、次のようにリダイレクト(>)を使用すればよい。

C:\> c#at ファイル1 ファイル2 ファイル3 > 連結したファイル

 c#atプログラムのソース・コードは次のようになる。

 1: // c#at.cs
 2:
 3: using System;
 4: using System.IO;
 5: using System.Text;
 6:
 7: public class CatSample {
 8:   public static void Main(string[] args) {
 9:
10:     string line;
11:
12:     foreach (string arg in args) {
13:       FileStream   fs = new FileStream(arg, FileMode.Open);
14:       StreamReader sr = new StreamReader(fs, Encoding.GetEncoding(932));
15:
16:       while ((line = sr.ReadLine()) != null) {
17:         Console.WriteLine(line);
18:       }
19:       sr.Close();
20:       fs.Close();
21:     }
22:   }
23: }    
C#版catコマンドであるc#atのソース・コード

StreamReaderクラス

 このプログラムでも、先ほどと同様にFileStreamオブジェクトを作成し、次にそれを第1パラメータとしてStreamReaderのコンストラクタを呼び出す。この際第2パラメータとしては、文字のエンコーディングに使用するEncodingオブジェクトを指定する。今回は、コードページ番号932によるエンコーディングを指定した。コードページ番号932はShift-JISに対応するもので、これによりStreamReaderオブジェクトは、バイト列である入力ストリームをShift-JISの文字として解釈するようになる。

13: FileStream fs = new FileStream(arg, FileMode.Open);
14: StreamReader sr = new StreamReader(fs, Encoding.GetEncoding(932));
まずFileStreamオブジェクトを生成し、それを基にStreamReaderオブジェクトを生成する

 実はこの2行は、StreamReaderクラスの別のコンストラクタを使用して、次のような1行に置き換えることができる。

StreamReader sr = new StreamReader(arg, Encoding.GetEncoding(932));
ファイル名を第1パラメータに指定するコンストラクタでは、FileStreamオブジェクトの生成を省略することができる

 このコンストラクタでは、第1パラメータにファイルのパス名を指定することで、FileStreamオブジェクトの生成を省略することができる。もちろんこの場合でも、内部的にはFileStreamオブジェクトが生成されているものと思われる。StreamReaderクラスは読み出し専用であるため、ファイルを開くときのファイル・モードは自明だ。

ReadLineメソッドとWriteLineメソッド

 StreamReaderオブジェクトでは、ReadLineメソッドによりファイルから行単位でデータを読み出すことができる。そしてReadLineメソッドは、読み出す行がなくなると(つまりファイルの終端に達すると)nullを返す。ここでは読み出した行を文字列変数に代入し、Console.WriteLineにより画面にその行を表示しているだけだ。ConsoleクラスはTextReader/TextWriterの派生クラスではないが、ReadLineやWriteLine、あるいはReadやWriteなどのメソッドを装備しており、TextReader/TextWriterクラスと同じようにして使用することができる。

10: string line;
       ・・・
16: while ((line = sr.ReadLine()) != null) {
17:   Console.WriteLine(line);
18: }
ファイルの終端に達するまで、ReadLineメソッドにより1行ずつ読み込み、Console.WriteLineで画面に出力する

 ちなみにStreamReaderクラスは、ストリームにまだ読み出せる文字が残っているかどうかをチェックするためのPeekメソッドを持っている。これを使用すれば、上の部分は次のように記述することもできる。

while (sr.Peek() != -1) {
  Console.WriteLine(sr.ReadLine());
}
ストリームにまだ読み出すべき文字があるかをチェックするPeekメソッドを使用した例

本当は必要な例外処理

 すでにお気づきのこととは思うが、今回紹介した2つのサンプル・プログラムでは、エラー処理をいっさい行っていない。例えば、プログラムの起動時に引数としてファイル名が指定されなかった場合や、指定されたファイルが存在しなかった場合などだ。このような状況で、今回のプログラムを実行すると、例外(exception)が発生しプログラムが強制終了される(環境によってはデバッガが起動されようとする)。本来ならば、try文やcatch文による例外処理が必要だが、今回はファイル入出力だけに着目するためこれらを省略した。例外処理については回を改めて解説する予定である。End of Article

更新履歴
【2002/03/06】 当初は.NET Frameworkのベータ版をもとにして記述していましたが、.NET Framework製品版がリリースされたので、本文および掲載しているソース・コードの一部を修正し、これに対応させました。

「C#プログラミングTips」

 



Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間