C#でのファイル入出力

出典: ソフメWiki

目次

C#のファイル入出力

C#ではファイルにデータを書き込んだり、ファイルからデータを読み込むのにストリームという物(クラス)を使用します
このストリームについてはとりあえず標準入出力やファイルなどに対してデータの書き込みや読み込みを行うための処理を行うクラス群だと思ってもらって大丈夫です。つまり今までコンソールに文字を表示したりしていたのも実はストリームだったというわけです。
これにはさらにデータをバイト単位で処理するバイトストリームや、文字単位で処理する文字ストリームといったものがあり、処理するデータによって使い分けていきます。
「細かく分かれていると一つ一つ覚えるのが大変そう…」と思う人もいるかもしれませんが、「ストリーム」として扱われるクラスは全て同じ様に扱う事が可能なのです。例えば今までコンソール画面に文字を出力していたWriteやWriteLineといったメソッドが、ファイルへの書き込みでもまったく同じように使用できます。そのため主要なメソッドを使えるようにしておけば様々なデータ処理を行う事が出来るようになります。
さて、色々あるストリームですが、普段使う上ではファイルから「文字」を読んだり書いたりして使う事の方が多いと思うので、この資料では初めにバイトストリームを軽く紹介した後、文字ストリームの方を細かく書いていきます。

基となるStreamクラス

最初に「ストリーム」は(主要なメソッドは)同名のメソッドを使用できると書きましたが、それは全てのストリームのスーパークラスとしてStreamクラスがあるためです。
このStreamクラスはバイトストリームを表すクラスであるため、メソッド等もバイトデータの書き込み・読み込みを目的として書かれています。これ以外のストリームは必要に応じてこのStreamクラスのメソッドをオーバーライドして、文字の書き込み機能に変更したりしているわけです。 Streamクラスのメソッドの内良く使うメソッドには以下の様な物があります。ただしこれらのメソッドは結局オーバーライドによって機能が変わってしまったりするので、頑張って覚えなくても大丈夫だとは思います。

int ReadByte()…ストリームの現在位置の次のバイトを読み込んで整数値で返す
int Read(byte[] buffer,int offset,int count)…現在位置の次のバイトから複数バイト読み込む
                                              読み込んだデータはbuffer[offset]から後ろに格納され、countのバイト数だけ読み込む
                                              戻り値として読み込みに成功したバイト数を返す
void WriteByte(byte value)…引数として渡した1バイトデータを出力ストリームに書き込む
int Write(byte[] buffer,int offset,int count)…buffer[offset]以降のバイトデータを、countで指定したバイト数だけ書きこむ
                                               戻り値として書き込みに成功したバイト数を返す
void Close()…ストリームを閉じる

読み書き及び終了を表す閉じるメソッドとしてこの5つがあります。「現在位置」というのはファイルを読み込んだり書き込んだりする時に、その処理を行うファイル内での位置(最初から、とか2行目の何バイト目とか…)です。
この位置は基本的には読み込んだり書き込んだりすると動きます。例えばファイルから1バイト読み込んだら、次に読み込む処理を行った時には次のバイトから、といった感じです。
また、以下のSeek()メソッドを使うと自由に移動させることができます。

long seek(long offset,SeekOrigin origin)…origin(列挙体)で指定した位置からoffsetだけ離れた位置に、現在位置を移動する,戻り値は移動した後の位置

※SeekOriginは列挙体です、列挙体については列挙体を参考にしてください

SeekOrigin.Begin…ストリームの初めから
SeekOrigin.Current…現在位置から
SeekOrigin.End…終わりから(後ろから数える)

のどれかを指定します。
ただし、使用するStreamによってはこのSeek()メソッドは使えない事があるので注意して下さい。

バイトベースのStream

なんだかんだとStreamについて書いてきましたが、そろそろ実際に使ってみましょう。まずは基本的なバイトストリームを使っていきます。 コンソールへの出力などについてはもうやってしまったので、ファイルに対してバイトデータを読んだり書いたりしてみましょう。
なお、「バイト」ストリームという名前の通り、ファイルから1バイトずつ読み取って何か処理をしますので、全角文字等の2バイト文字はちょっと工夫をしないと正しく読み取れませんから注意しましょう。2バイト文字を簡単に読み取りたい場合は文字ストリームを使います。

FileStreamクラス

バイトストリームにもいろいろありますが、ファイルへの読み書きを行う基本的なクラスにFileStreamクラスがあります。
FileStreamはその名の通りバイト単位でファイル入出力を行う機能を持ったクラスです。
※バイトストリームは頻繁に使用する物でも無く、少しとっつきづらい内容でもあります。いまいち理解できないようでしたら後の文字ストリームを先に読んで、ストリームに慣れてみてください。
とりあえず読み込むファイルが無いといけないので以下の様な内容のテキストファイル「test.txt」をプログラムの実行ファイルと同じ場所に作ります。

FileStreamTest!
Hello,Stream!

まあ内容は自由に書いてください。ただし全角文字は使わないでくださいね。
それでは、ファイルの内容を読み込みながら、コンソール画面に表示していくプログラムを作ってみます。初めに例外等が何も起こらないと仮定して、エラー処理の無いコードを見てみましょう

  1. using System;
  2. using System.IO; //Streamを使うために必要です
  3.  
  4. class StreamPractice
  5. {
  6.     public static void Main()
  7.     {
  8.         //ファイル名とFileMode列挙体でファイルをどう扱うのかを設定する
  9. 	FileStream fs = new FileStream("test.txt", FileMode.Open);
  10. 	//ファイルの末尾に行くまで読み込む
  11. 	int i;	//読み込んだバイトデータを受け取る変数
  12.         while ((i = fs.ReadByte()) != -1)
  13.         {
  14.             //受け取ったバイトデータをその数値が表す文字にして表示する
  15.             Console.Write((char)i);
  16.         }
  17. 	//終わったら閉じてメモリを解放(Dispose)する
  18. 	fs.Close();
  19. 	fs.Dispose();
  20.     }
  21. }

実行結果は以下のようになります

FileStreamTest!
Hello,Stream!

きちんとファイルの中身が表示されました。コンパイルが出来ない人は2行目のusing宣言を追加しているか確認してみてください。
このusing System.IOというのは、StreamクラスがSystem.IO名前空間(については追々解説資料を作る予定ですが…)にあるため、宣言をしないとStreamが使えないために書いてあります。
それでは上から見ていきましょう、まず9行目の

FileStream fs = new FileStream("test.txt", FileMode.Open);

でFileStreamオブジェクトのインスタンスを作成しています、FileStreamクラスのコンストラクタは沢山ありますが、今回のようにファイル名を指定する場合には

FileStream(ファイル名,FileMode列挙体)

のコンストラクタが一番良く使う物になります。新しい列挙体が出てきましたが、意味としては簡単です
FileModeはファイルを開くのか、新しく作るのか、既存のファイルに追加していくのか…等の動作を設定できる列挙体で、良く使うメンバには以下の3つがあります

FileMode.Open…既存ファイルを開く。ファイルが無いとFileNotFoundExceptionが発生する
FileMode.OpenOrCreate…既存ファイルを開く。ファイルが無い時は新しく作成する
FileMode.Create…新しくファイルを作る、既にファイルがある場合上書きされる(そのファイルが隠しファイルだと例外が発生する)

※実際にはこれ以外にもメンバがあったり、読み込み専用等設定できるコンストラクタもありますが、バイトストリームは資料内ではほとんど使用しませんので省略します。

今回のプログラムではFileMode.Openを使ってファイルを開いたわけです。このファイルからデータを読み込む処理は12行目の

while ((i = fs.ReadByte()) != -1)

から始まります。このwhileの条件を整理すると

1.int型変数iに、FileStreamのReadByteメソッドの戻り値を格納する
2.その値が-1でなければループを続ける

という流れになっています。
ReadByteメソッドは特にオーバーライドされていませんので、Streamクラスと同じで1バイト読み込み、その内容を整数値として返します。
テキストに書いてあるのは文字であるのに、バイトを読み込んで整数値として返す…といわれるとこんがらがるかもしれませんが、これは文字コードの話になります。
コンピュータ内部では「文字コード」という物が設定されており、WindowsではShift-JISという文字コードが一般的に使われています。「文字コード」とは簡単に言えば様々な文字にどのような数値をあてはめるかの決まりみたいなもので、UnicodeとかASCII等色々種類があります。コンピュータ内部では文字であっても数値で表現することしかできないため、この様な物があります。
例えば「A」という文字はShift-JISでは数値で65をあてはめるため、ReadByte()メソッドにより"A"を読み込むと「65」という数値が戻されます。そして文字コードは0から始まるため、文字が無い場合は-1を返すことでこれ以上文字が無いという事を判断できるというわけです。

ループの条件は今の理由から、読み込める文字が無くなるまでという事が分かりました。ではループの中でやっている事を見てみましょう。

Console.Write((char)i);

この行ではReadByteで受け取った整数値を文字を表すchar型にキャストしています。先程も書いたとおり「文字」には「数値」があてはめられているため、int型をchar型にキャストすると、その数値が表す文字へと変換する事が出来るのです。先程の例で言えば「65」という数値を文字に変換すると「A」となる訳です。これを繰り返して文字を出力して行っています。

最後に、Streamを使用し終えた後は必ず

Close()→Dispose()

の順番でメソッドを呼び出し、Streamを閉じてください。
Close()はStreamの使用を終了して閉じるメソッドで、Dispose()はStreamに割り当てていたメモリを解放するメソッドです。
メモリを解放、の当たりを説明すると長くなってしまう上に、そこは本質ではないので省略します。Streamを使用した際にはこれらのメソッドを呼び出さなければならないと覚えておきましょう。


さて、突然ですがこれでバイトストリームの解説はお終いです。本当は他にもさまざまなメソッドがあったりしますが、使用機会としては少ないことと、バイトデータに慣れていないと混乱しがちなのでこれぐらいで終わりにしておきます。バイトがどうも取っつきづらいと感じた人はこの先の文字ストリームの方でストリームになれていきましょう。
また、今回のバイトストリームのサンプルプログラムでは「例外が起こらないと仮定して」と書きました。実際にはファイル操作は書き込みや読み込みに失敗したり、ファイルが見つからない等の例外が起こりやすいので、例外処理(try~catch)を行わなくてはなりません。それについても文字ストリームを解説しながら並行して解説していきます。

文字ストリーム

バイトストリームは1バイトずつデータを読み書きしました(といっても書き込みはやっていませんが…)
ということは日本語のテキストファイルは読み込めません。というより全角文字は2バイトなので普通にやると読み込めません。英語しか書かないのであれば良いですが日本語を使いたい場合も多々あります。そう言う場合に使うのが文字ストリームです。
文字ストリームは名前の通り「文字」を単位として読み書きを行うため、半角/全角関係なく読み書きが可能です。

文字ストリームではStreamReaderを使って読み込み、StreamWriterを使って書きこむのが基本です。早速これらを使ってファイルからデータを読んだりデータを書き込んだりしてみましょう。

StreamReaderクラス

StreamReaderクラスを使うとファイル等の内容を簡単に読み込めます。読み込みの際に良く使うメソッドは以下の4つがあります

int Read()…1文字読み込んで文字コードを返す
string ReadLine()…1行読み込んで読み込んだ文字列を返す(改行文字は含まれない)
string ReadToEnd()…ストリームの内容を全て読み込んで文字列として返す
int Peek()…次の文字を読み込んで文字コードを返すが、その文字は使用しない。ファイルにまだ文字があるか調べる時に使用する。

他にもメソッドやプロパティが沢山ありますが、単に読み込むぐらいならこの4つあたりを知っておけば充分だったりします。
今回は以下の様な内容の「test2.txt」を作成し読みこんでみます。

こんにちは
このテキストはC#講座で使用します
ありがとうございました。

まあ内容は何でも良く、今回は日本語を打ちこんでもOKです。
ではStreamReaderを使って読み込んでみましょう。まずは単純にファイルの内容をいっぺんに読み込んで表示するプログラムです。
また先程は例外処理を省きましたが今回は簡単に行う事にしますので、その辺も解説していきます。

  1. using System;
  2. using System.IO; //Streamを使うために必要です
  3.  
  4. class ReaderPractice
  5. {
  6.     public static void Main()
  7.     {
  8. 	StreamReader sReader = new StreamReader("test2.txt");
  9. 	try
  10. 	{
  11. 	    string result = sReader.ReadToEnd();	//全て読み込む
  12. 	    Console.WriteLine(result);	//表示
  13.  
  14. 	    sReader.Close();	//ストリームを閉じる
  15. 	    sReader.Dispose();	//ストリームをメモリから解放する
  16. 	}
  17. 	catch (IOException e)
  18. 	{
  19. 	    Console.WriteLine(e.Message);	//エラーメッセージ出力
  20. 	}
  21.     }
  22. }

さてとりあえず実行してみると多分以下の様になると思います。(きちんと表示される人もいるかもしれません)

????????
????e?L?X?g??b???u????g?p?????
???肪???????????????B

物凄くバグって見えますが動作としては正常です、これも文字コードの関係で出力が変になっているのですが、これの対処法は後で書きますのでとりあえずプログラムを見ていきましょう。
まず8行目でStreamReaderクラスのインスタンスを生成しています。今回はファイル名を指定するだけのコンストラクタを使用しています。
StreamReaderのコンストラクタは後でもう一つ紹介しますが、沢山のオーバーロードがあります。流石に全部紹介すると長くなる(し覚えられない)ので割愛します。
ファイル名を指定するコンストラクタを使用すると、そのファイルを開く事が可能です。ファイルが存在しない場合はFileNotFoundExceptionが発生します。ファイルが無い場合何か処理をする時はここで例外処理をしても良いでしょう。

11行目でファイルの内容を全て読み込むReadToEndメソッドを使用しています。読み込んだ内容はstring変数resultに格納しています。
12行目でその内容を画面に出力し(文字化けしますが…)、13,14行目でStreamReaderを閉じて、メモリから解放しています。この処理は忘れずに行いましょう。

また例外処理として17行目のcatch文の中でエラー内容をコンソールに表示しています。ファイルを読み込む際の例外には先程のファイルが見つからない他には、ファイルを読み取る権限が無い等の原因があります。こういったファイル処理等のI/O処理に付随する例外はIOExceptionという例外である事が多いため、今回はその例外をcatchするcatch文を使用しています。
今回は例外内容を表示するだけですが、ファイル入出力に失敗したら何か処理をして、別の例外の場合プログラムが終了すると言った事をする場合この様に詳細な例外クラスを使用してcatch文を書く方法があります。

文字コードを指定して読み書きする

さて、先程実行結果で見せたように、ファイルから読み込んだ内容を表示したら文字化けしてしまいました。
これは別にStreamReaderが日本語を使えないわけでは無く、C#で使用される文字コードとWindowsの文字コードがあっていないため、Windowsで作ったテキストファイルを読み込んだ時に文字化けを起こしてしまうのが原因です。具体的に言うとWindowsではShift-JISという文字コードを使用しますが、C#ではUnicodeという文字コードを採用しているためです。

とはいえ、テキストファイルを作るたびにいちいち文字コードをUnicodeに変換するのは面倒です。幸いにもStreamReaderでは読み込みに使用する文字コードを設定する事が出来ます。文字コードを指定したStreamReaderは以下のコンストラクタを使用する事で生成できます。

StreamReader(string path,System.Text.Encoding encoding)

最初のpathは先程と同じファイル名です、2つ目のEncoding(System.Text内にあります)というクラスを使用して文字コード(文字エンコード)を指定できます。
Encodingというのは初めて出てきましたが例えば以下の様に書くとUnicode用(元々ですが例として)のStreamReaderになります。

//プログラムの初めにusing System.Textが書かれているものとしています
//usingを書かない場合「System.Text.Encoding.Unicode」と書けば同じ意味になります
StreamReader sReader = new StreamReader("test2.txt",Encoding.Unicode);

この様に文字コードを指定するのがEncodingクラスの一つの使い方です。なおEncodingクラスのプロパティ等は静的(static)であるため、クラス名.プロパティやメソッド名として呼び出せます。
指定のために使えるプロパティで良く使うのは以下のプロパティです。

Encoding.ASCII…ASCIIコード
Encoding.Default…使用OSの標準文字コード
Encoding.Unicode…Unicode
Encoding.UTF8…UTF8コード

基本的にはこの様なプロパティから使用するエンコードを選択するだけで使用できます。
しかし「WindowsはShift-JIS」と書いた割にShift-JIS用のプロパティがありませんね。Shift-JISを指定する場合は2つ方法があります。
一つは今書いたDefaultを指定する事です。Windowsの標準文字コードはShift-JISなので、Defaultを指定するとShift-JISになります。
もしくはGetEncodingというメソッドを使用してShift-JIS文字コードを表すEncodingクラスを取得します。これは以下の様に書きます。

Encoding.GetEncoding("shift_jis");

または

Encoding.GetEncoding(0);

メソッドの引数に取得したい文字コード名またはコードページと呼ばれるその文字コードを表す番号を書きます。
このGetEncodingを使うと、プロパティで用意されていない文字コードを使用する事が出来るようになりますが、その文字コードの種類と名前・コードページは膨大な量ありますのでここではShift-JISだけにしておきます。「Encoding.GetEncoding」とかで検索をすれば色々出てくるかと思いますので必要に応じて各自で検索をしてみてください。

さて、長々とEncodingの説明をしましたが、この辺が分かればStreamReaderに文字コードを設定するのは簡単です。 先程のプログラムの3行目に

using System.Text;

と書いてから、8行目のコンストラクタを

new StreamReader("test2.txt",Encoding.GetEncoding("shift_jis"));

に変更してみてください。今度はファイルの内容が文字化けせずに画面に表示されるはずです。

StreamReaderクラス(続き)

さて、文字コードを設定できるようになった所で、StreamReaderを使った例をもう一つ紹介します。今度は1行ずつ読み込む例です。

  1. using System;
  2. using System.IO; //Streamを使うために必要です
  3. using System.Text;
  4.  
  5. class ReaderPractice
  6. {
  7.     public static void Main()
  8.     {
  9. 	StreamReader sReader = new StreamReader("test2.txt", Encoding.GetEncoding("shift_jis"));
  10.  
  11. 	try
  12. 	{
  13. 	    while (sReader.Peek() >= 0)	//次に文字があればループを続ける
  14. 	    {
  15. 		string result = sReader.ReadLine(); //1行読み込む
  16. 		//普通はこの間で何かしら文字列処理をする
  17. 		Console.WriteLine(result);	//表示
  18. 	    }
  19.  
  20. 	    sReader.Close();	//ストリームを閉じる
  21. 	    sReader.Dispose();	//ストリームをメモリから解放する
  22. 	}
  23. 	catch (IOException e)
  24. 	{
  25. 	    Console.WriteLine(e.Message);	//エラーメッセージ出力
  26. 	}
  27.     }
  28. }

実行結果は先程と同じですが

こんにちは
このテキストはC#講座で使用します
ありがとうございました。

となります。プログラムで変化した部分はtry文の初めの所(13行目~18行目)です。
分かりづらいのは13行目のwhile文の条件です。前に書いた様にPeek()メソッドは次の文字の文字コードを返し、文字が無ければ-1を返します。 これを利用して、返された数値が0以上なら文字があるため読み込みを続け、0未満(-1)であったら終了すると言った分岐を行います。
文字があれば15行目の処理に入ります。ここではStreamReaderのReadLine()メソッドを使用してファイルから1行分の内容を読み込み。変数resultに代入しています。
今回はこの変数をそのまま表示しているだけなので一つ前の全内容を一気に取得したものと結果が同じで、そちらの方が処理も速いです。しかし一行ずつ読み込んでいく場合、ファイルの内容一行ごとに何か文字列処理を行う(置き換えや文字列のチェックなど)ができます。そう言った必要がある場合はこのReadLine()メソッドを使用しましょう。

StreamWriterクラス

次はファイルに文字や数字を書き込んでみましょう。書き込みにはStreamWriterクラスを使用します
書き込みの際には以下の様なStreamWriterクラスのメソッドを使用します

Write(intやstring等)…引数で指定した文字列や数値等を書き込む
WriteLine(intやstring等)…引数で指定した文字列や数値等を書き込み改行する

読み込みと比べて少なく、名前や内容もコンソールへの出力と同等なので理解しやすいと思います。

さて、メソッドの次にコンストラクタも紹介します。先程StreamReaderではファイル名のみのコンストラクタと文字コードを指定したコンストラクタの二つがありました。StreamWriterでも文字コードを指定しないと困るときは多いので、2つのコンストラクタを紹介します。まずは1つ目です

StreamWriter(string path)

このコンストラクタは作成したい・あるいは上書きしたいファイル名を指定するだけです。今回は書き込みですのでファイルが存在しなくても新しく作成するので問題はありませんが、ファイルが存在する場合は上書きされてしまうので注意が必要です。
次のコンストラクタを使用すると文字コードの他に、上書きするか否かも設定できます。

StreamWriter(string path,bool append,Encoding encoding)

引数が二つ増えてますね。最後のencodingはStreamReaderの時と同じ方法で文字コードを指定できます。真ん中のappendはなんでしょうか?
これが上書きを行うか否かを設定する引数です。この値がtrue/falseによって以下の様に動作が変化します

trueの場合…指定したファイルが既に存在した場合、そのファイルを開き、ファイルの末尾に追加で書き込んでいく
falseの場合…指定したファイルが既に存在した場合、ファイルを上書き(削除して新しく作成)する

と、このような感じです。

前フリが長かったかもしれませんが、これだけわかればファイルへの書き込みは十分可能です。今書いたメソッドやコンストラクタを使用した簡単なサンプルを書いてみます。

  1. using System;
  2. using System.IO; //Streamを使うために必要です
  3. using System.Text;
  4.  
  5. class WriterPractice
  6. {
  7.     public static void Main()
  8.     {
  9. 	//StreamWriterでファイルを開く(文字コードをShift-JISにして、既存のファイルの場合上書きを行う)
  10. 	StreamWriter sWriter = new StreamWriter("kouza.txt", false, Encoding.GetEncoding("shift_jis"));
  11. 	try
  12. 	{
  13. 	    sWriter.WriteLine("こんにちは");		//書き込んで改行
  14. 	    sWriter.WriteLine(35);
  15. 	    sWriter.Write(true);		//bool型も書き込めます。改行無し
  16. 	    sWriter.Write("とぅるー");	//前で改行していないので「trueとぅるー」くっつきます
  17.  
  18. 	    //閉じるのと解放を忘れずに
  19. 	    sWriter.Close();
  20. 	    sWriter.Dispose();
  21. 	}
  22. 	catch (IOException e)
  23. 	{
  24. 	    Console.WriteLine(e.Message);	//エラーメッセージ出力
  25. 	}
  26.     }
  27. }

実行しても特に何も表示されませんが、実行ファイルと同じディレクトリに「kouza.txt」が作られたはずです。ファイルを開くと

こんにちは
35
Trueとぅるー

と書かれているはずです。色んな文字や数字をそのまま引数にしたり、変数を引数にしたり、計算を引数にしたりできます。色々やってみましょう
さて、プログラムの内容ですが読み込みと比べてかなり分かりやすいと思います。
10行目で先ほど紹介した2番目のコンストラクタを使用して、StreamWriterクラスのインスタンスを作っています。コメントにもあるとおりShift=JISを文字コードとして使用し、上書きを行う設定でkouza.txtというファイルを作成します。
try文の中でファイルに書き込みを行っているわけですが、ぱっとみて分かるとおりコンソール画面に出力するConsole.WriteLineやWriteと何ら変わりがありません。それどころかやっていることも同じで、出力先がコンソールかファイル(kouza.txt)かの違いだけです。Write()を使った場合次に書き込んだ文字列がくっついて書き込まれることに気をつければ、まあ大丈夫だと思います。
最後にこれもStreamReaderと同じでファイルを閉じる→解放(Disposeメソッド)を行ないます。忘れないようにしましょう。

ここまででファイルへの文字ストリームを使用した読み込み・書き込みの解説が終わりました。Cの時とどっちが分かりやすいかは結構個人差があるかと思います。C#でファイルの読み書きをする場合は文字コードに気をつけないと文字化けしてしまうことに注意しましょう。

usingとメモリの解放

本題は終わりましたが、StreamReaderやWriter等、「Dispose」というメソッドを持つクラスを使用するときに大切な処理があるので、ここで一緒に紹介します。
さて、とりあえず以下のコードを見てください。

  1. using System;
  2. using System.IO;
  3. using System.Text;
  4.  
  5. class WriteErrorTest
  6. {
  7.     public static void Main()
  8.     {
  9. 	StreamWriter sWriter = new StreamWriter("error.txt", false, Encoding.GetEncoding("shift_jis"));
  10. 	try
  11. 	{
  12. 	    int n1 = 8, n2 = 0;
  13. 	    sWriter.WriteLine(n1 / n2);		//割った結果を書き込む…?
  14.  
  15. 	    sWriter.Close();
  16. 	    sWriter.Dispose();
  17. 	}
  18. 	catch (Exception e)  //すべての例外を捕まえる
  19. 	{
  20. 	    Console.WriteLine(e.Message);	//エラーメッセージ出力
  21. 	}
  22.     }
  23. }

このコードは実行こそ出来ますが、エラーメッセージが表示され終了します。それもそのはず、n2の値が0なので0で割り算をした結果を書き込もうとしたときに例外が発生するからです。
「そんなこと分かってるよ!」と思った方も多いでしょうが、ではこの時変数sWriterのClose()やDispose()は実行されるでしょうか?
答えは「されない」です。「n1 / n2」が実行された時点で処理はcatchの中に移ってしまうからです。一つの対応策としては以下の様にfinallyを使用する手があります。

  1. using System;
  2. using System.IO;
  3. using System.Text;
  4.  
  5. class WriteErrorTest
  6. {
  7.     public static void Main()
  8.     {
  9. 	StreamWriter sWriter = new StreamWriter("error.txt", false, Encoding.GetEncoding("shift_jis"));
  10. 	try
  11. 	{
  12. 	    int n1 = 8, n2 = 0;
  13. 	    sWriter.WriteLine(n1 / n2);		//書き込んで改行
  14. 	}
  15. 	catch (Exception e)
  16. 	{
  17. 	    Console.WriteLine(e.Message);	//エラーメッセージ出力
  18. 	}
  19. 	finally
  20. 	{
  21. 	    sWriter.Close();
  22. 	    sWriter.Dispose();
  23. 	    Console.WriteLine("メモリを解放しました!");
  24. 	}
  25.     }
  26. }

finallyブロックは例外が発生しても発生しなくても必ず実行されるブロックです。これなら例外が発生したとしてもファイルを閉じる処理、メモリを解放する処理が行われるので安全です。しかしこう毎度毎度finallyブロックを書くのも大変です、書き忘れてメモリ解放がされないと、自動で解放されるまでファイルは開きっぱなしの状態になり削除ができなくなったりして困ったことになります。
これをもう少し楽に(なったと感じるかは人それぞれですが…)するためにusing ステートメントをつかいます。
usingはソースの上の方に書いてますが、これはCのincludeに当たる処理をしています。まだ解説していませんが名前空間という物をその後に指定することでそこに含まれるクラス等が使えるようになります(ヘッダファイルをイメージしても良いと思われます)。
usingにはもう一つ使い方として、先程から言っているメモリの解放を保証するための構文としての使い方があります。

…と言われてもイメージできないと思いますので、とりあえず先程のプログラムをそのusingステートメントとやらを使用したものに書き換えてみます。分かりやすくするためのCloseメソッドを省略しますのでまだ完璧ではありません。

  1. class WriteErrorTest
  2. {
  3.     public static void Main()
  4.     {
  5. 	StreamWriter sWriter = new StreamWriter("error.txt", false, Encoding.GetEncoding("shift_jis"));
  6. 	//usingステートメントでsWriterの解放を保証します
  7. 	//このブロック({}内)を出たり、例外が発生した場合自動的にDispose()メソッドを呼び出します
  8. 	using (sWriter)
  9. 	{
  10. 	    try
  11. 	    {
  12. 		int n1 = 8, n2 = 0;
  13. 		sWriter.WriteLine(n1 / n2);		//書き込んで改行
  14. 	    }
  15. 	    catch (Exception e)
  16. 	    {
  17. 	        Console.WriteLine(e.Message);	//エラーメッセージ出力
  18. 	    }
  19.         }
  20.     }
  21. }

少しすっきりしました。Dispose()メソッドの呼出がなくなり、そのかわりに8行目に

using(sWriter)

と書かれています。これがusingステートメントです。これを書くと「usingに続くブロック内({ }の中身)の処理が終わるか、例外が発生するなどしてこのブロックから処理が抜けると自動的に()内に指定したオブジェクトのDispose()メソッドを呼び出す」という意味になります。
大雑把にいってしまえばさっき書いたfinallyの中にDispose()を書いていたところを、自動的にやってくれるということです。usingの中に指定できるのはDispose()メソッドを持っているクラスのみとなるので注意しましょう。
ちなみに今回は分けていますがusingの中で直接

using(StreamWriter sWriter = new StreamWriter("error.txt", false, Encoding.GetEncoding("shift_jis"));

と書いても問題はありません。

※Dispose()メソッドを持っている=IDisposableインターフェースを実装している、とも言います。というよりこの言い方が正しいのですがまだインターフェースについて解説するページを書いていないので今はそういう事で覚えておきましょう

これで確実にDispose()が呼び出されます。Disposeメソッドしか持っていないクラスならこれでいいのですが、StreamReaderやStreamWriterはClose()メソッドを実行してファイルを閉じてから解放しないといけません。こちらはusingのように便利なものはないのでやはり以下の様にfinalyを追加します。catch(Exception e)の後に以下のfinallyを追加しましょう

  1. finally
  2. {
  3.     if (sWriter != null)
  4.     {
  5. 	sWriter.Close();
  6.     }
  7. }

今回は必要無いですが、例外の発生状況によって解放対象のオブジェクトが生成される前にfinallyにはいる可能性がある場合、このようにnullかどうかの判定をする事が多いです。これも一応覚えておきましょう。

番外:ファイル名指定時の注意

最後にちょっとした番外編です。ファイル名を指定する際今回の例では全て実行ファイルと同じディレクトリのファイルを指定しました。
では実行ファイル以下の「abc」というフォルダ内の「def.txt」ファイルを指定する場合どうするでしょうか、ファイル名に以下の様に指定するとどうなるか考えてみましょう。

"abc\def.txt"   //「abc」の「def.txt」を指定したことになる?

さあどうでしょうか、これはできません。コンパイルすると「認識できないエスケープシーケンスです」と言われるでしょう。
別にコンパイラがおかしいのではなくて、「\」という文字は「\n」や「\t」等エスケープシーケンスとして使用されるのでそのままでは文字列に含めません。以下の様に

"abc\\def.txt"

「\\」にする必要があるのはC言語と同じです。
しかしもっと深いディレクトリにファイルを作ろうとした場合は「\\」が大量に並ぶことになります。入力も大変で「\\\」になってしまったり癖で[\」を打ってしまったり…。
こういったファイル名を打つときは便利だったエスケープシーケンスは大変厄介な存在になります。これを解決するのが「@」です。以下の様に

@"abc\def.txt"

と打ち込んでみましょう、コンパイルも実行も出来るようになります。
文字列の前に「@」を付けるとその後の文字列内では全てのエスケープシーケンスが無効になります。つまり「\n」をいれようが「\t」をいれようがそのまま出力されるようになります。

ファイル名の指定なんかをするときは大変便利ですので覚えておきましょう。

個人用ツール