今回位は、「結合実行ボタンクリック処理」を、いくつかのアルゴリズムで置き換えてみたいと思います。
あくまでも参考情報というこのなのでプロジェクトのZIPファイルは用意していません。
「こういう方法もあるんだ」という事で読み進めて頂ければOKですが、動作確認がしたい方は、前回のソースコードの該当部分(UxExecJoin_Clickイベントハンドラ)を、今回掲載するソースコードで置き換えて実行してみて下さい。
フラグを使った方法(前回の復習)
結合対象のファイルが1個目か2個目以降かによって、
- 1個目はヘッダを出力するが、2個目はヘッダを出力しない
- 1個目はファイルを上書きするが、2個目は追加する
という処理を分ける必要があります。
そこで、 first_file_flg という bool型の変数を用意し、この値が true か false かによって、読み込んだファイルが1個目か2個以降かを判断していました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
bool first_file_flg = true; foreach(var file in files) { //省略 if(first_file_flg) { //省略 first_file_flg = false; } else { //省略 } } |
フラグの代わりにカウント変数を使って判定する
1個目と2個目を判定したいだけですから、フラグではなくファイル数をカウントして、カウント数が0なら1個目、0以外なら2個目という風に判定できますよね。
ということで、数を数える変数 cnt を用意し、ループの最後に cnt に1を加算することで、1個目か2個目以上かを判定するアルゴリズムが次のものになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int cnt = 0; foreach(var file in files) { //省略 if(cnt == 0) { //省略 } else { //省略 } cnt ++; } |
普通に使う分には、この方法でも問題はありません。
整数数値型 – C# リファレンス | Microsoft Docs
int型の変数が扱える値は -2,147,483,648 ~ 2,147,483,647(マイナス21億~+21億)です。 cnt を加算し続けると 21億で符号が反転し、やがて 0 に戻ってしまいますが、そもそもそんな数をファイルを1つのフォルダに置けないので、実用上問題はありません。 ちなみに今は if(cnt == 0) としていますが、 if(cnt > 0) と記述してもいいと思います。 |
以上の方法でイベントハンドラを書き換えたソースコードは以下の様になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
private void UxExecJoin_Click(object sender, EventArgs e) { //ヘッダ行数の取得 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int val)) ? val : 0; //結合対象CSVファイルの文字コード取得 var target_enc = Encoding.GetEncoding(uxTargeCharCode.Text); //出力CSVファイルの文字コード取得 var output_enc = Encoding.GetEncoding(uxOutputCharCode.Text); //結合対象フォルダにあるファイル名を全て読み込む var files = Directory.GetFiles(uxTargetFolder.Text); //ファイル数を数えるカウント用変数に0をセット int cnt = 0; //ファイルの数だけループ foreach(var file in files) { //ファイルを改行で分割し、List形式に変換 var lines = File.ReadAllLines(file, target_enc).ToList(); //最初のファイルか否かで処理分岐 if(cnt == 0) { //最初のファイルの場合、ヘッダごとファイル保存(既にファイルがあれば上書き) File.WriteAllLines(uxOutputFile.Text, lines, output_enc); } else { //2個目のファイル以降は、ヘッダ行数を削除 for(int i = 0;i < head_cnt;i ++) { //Listの先頭行を削除 lines.RemoveAt(0); } //追加モードでファイル保存(既存ファイルに追加していく) File.AppendAllLines(uxOutputFile.Text,lines, output_enc); } cnt++; } |
Forループの添え字を使って判定する
「1づつ加算する cnt 変数で1個目のファイルか2個目のファイルかを判断するんだったら、いっそForループにしてしまって、添え字で判断したらよくね?」
という意見が聞こえてきそうですが、まさにその通りです。
今は foreach を使っていますが、 for に変更すれば、わざわざ判定用の変数を用意する必要がなくなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int cnt = 0; for(int i = 0;i < files.Length;i ++) { //省略 if(i== 0) { //省略 } else { //省略 } cnt ++; } |
for ループの中で files.Length という記述がありますが、これは配列変数の要素数を取得するプロパティです。
配列変数はLinqのCount() メソッドが使えますので、 files.Count() と記述することが出来ます。
Linq についてはこちらに簡単に触れていますので、目次から「Linqとは」を参照して頂ければと思います。
ちなみに、List や Dictionaryのクラスの要素数を求める場合は、Countプロパティを使いますので、この点はご注意ください。
ListやDictionaryにもLinqが使えるので、Count() メソッドも使えますが、通常はCountプロパティの方を使います。
では、for ループに書き換えたソースコードは以下の様になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private void UxExecJoin_Click(object sender, EventArgs e) { //ヘッダ行数の取得 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int val)) ? val : 0; //結合対象CSVファイルの文字コード取得 var target_enc = Encoding.GetEncoding(uxTargeCharCode.Text); //出力CSVファイルの文字コード取得 var output_enc = Encoding.GetEncoding(uxOutputCharCode.Text); //結合対象フォルダにあるファイル名を全て読み込む var files = Directory.GetFiles(uxTargetFolder.Text); //ファイルの数だけループ for(int cnt = 0; cnt < files.Length; cnt++) { //ファイルを改行で分割し、List形式に変換 var lines = File.ReadAllLines(files[cnt], target_enc).ToList(); //最初のファイルか否かで処理分岐 if(cnt == 0) { //最初のファイルの場合、ヘッダごとファイル保存(既にファイルがあれば上書き) File.WriteAllLines(uxOutputFile.Text, lines, output_enc); } else { //2個目のファイル以降は、ヘッダ行数を削除 for(int i = 0;i < head_cnt;i ++) { //Listの先頭行を削除 lines.RemoveAt(0); } //追加モードでファイル保存(既存ファイルに追加していく) File.AppendAllLines(uxOutputFile.Text,lines, output_enc); } } } |
追加書き込みだけで済ませる
今までのアルゴリズムでは、1回目はFile.WriteAllLines、2回目以降はFile.AppendAllLines を使いました。
今回はFile.AppendAllLines だけで済ませようというものです。
考え方は簡単で、最初に出力ファイルが存在していれば、削除するということです。
ファイルの存在確認と削除は以下の様に記述できます。
1 2 3 4 |
if(File.Exists(uxOutputFile.Text)) { File.Delete(uxOutputFile.Text); } |
一目瞭然ですが、Existsメソッドがファイルの存在確認、Deleteメソッドがファイルの削除です。
これで if 文によるFile.WriteAllLinesとFile.AppendAllLinesの切り替えは無くなりましたが、ヘッダに関しては1個目か否かの判断が必要なので、それは残ってしまいます。
では、実際にこの方法で書き換えたソースコードのご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
private void UxExecJoin_Click(object sender, EventArgs e) { //ヘッダ行数の取得 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int val)) ? val : 0; //結合対象CSVファイルの文字コード取得 var target_enc = Encoding.GetEncoding(uxTargeCharCode.Text); //出力CSVファイルの文字コード取得 var output_enc = Encoding.GetEncoding(uxOutputCharCode.Text); //結合対象フォルダにあるファイル名を全て読み込む var files = Directory.GetFiles(uxTargetFolder.Text); //出力ファイルが存在すれば削除する if(File.Exists(uxOutputFile.Text)) { File.Delete(uxOutputFile.Text); } //ファイルの数だけループ for(int cnt = 0;cnt < files.Length;cnt ++) { //ファイルを改行で分割し、List形式に変換 var lines = File.ReadAllLines(files[cnt], target_enc).ToList(); if (cnt == 0) { //2個目のファイル以降は、ヘッダ行数を削除 for (int i = 0; i < head_cnt; i++) { //Listの先頭行を削除 lines.RemoveAt(0); } } //追加モードでファイル保存(既存ファイルに追加していく) File.AppendAllLines(uxOutputFile.Text,lines, output_enc); } } |
Forループから if 文を排除してみる
今度は、ファイルが1個目か否かの判断をForループから排除して、その部分をシンプルにしたいと思います。
考え方としては
- 結合対象フォルダのファイル数が0より大きい場合、そのファイルを読んで出力ファイル名で上書き保存する(WriteAllLinesを使う)。
- 残りのファイルについては、ループ処理にてヘッダを削除しながら追加書き込みする。
ということになります。
残りのファイルをループ処理する際、既に1個目は処理済みになっているので、マイナス1した回数ループする必要があります。
そこで、For ループの開始を1、ループ回数を Length -1 にしています。
以上の内容で書き換えたソースコードは次の様になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
private void UxExecJoin_Click(object sender, EventArgs e) { //ヘッダ行数の取得 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int val)) ? val : 0; //結合対象CSVファイルの文字コード取得 var target_enc = Encoding.GetEncoding(uxTargeCharCode.Text); //出力CSVファイルの文字コード取得 var output_enc = Encoding.GetEncoding(uxOutputCharCode.Text); //結合対象フォルダにあるファイル名を全て読み込む var files = Directory.GetFiles(uxTargetFolder.Text); //ファイルがあれば最初の1ファイルを読んで出力ファイルに上書きする if (files.Length > 0) { var lines = File.ReadAllLines(files[0], target_enc).ToList(); File.WriteAllLines(uxOutputFile.Text, lines, output_enc); } //残りのファイルをループで処理 for(int cnt = 1;cnt < files.Length - 1;cnt ++) { //ファイルを改行で分割し、List形式に変換 var lines = File.ReadAllLines(files[cnt], target_enc).ToList(); //2個目のファイル以降は、ヘッダ行数を削除 for (int i = 0; i < head_cnt; i++) { //Listの先頭行を削除 lines.RemoveAt(0); } //追加モードでファイル保存(既存ファイルに追加していく) File.AppendAllLines(uxOutputFile.Text,lines, output_enc); } } |
File.WriteAllLines が復活してしまいましたが、ループ処理は見やすくなったのではないかと思います。
ヘッダを除外できるファイル読み込みメソッドを自作してみる
それでは、最後にヘッダを除外できるファイル読み込みメソッドを自作して、それを使ってみましょう。
こうすることによって、結合実行ボタンクリック時のイベントハンドラがスッキリします。
ここでのポイントはファイルから1行ずつファイルを読み込むという行為です。
ファイルを読み込む場合 StreamReaderクラスを、ファイルを書き込む場合、StreamWriter クラスを使います。
今回はファイルを読み込む部分を別メソッド化するため、StreamReaderクラスだけを使用しました。
ファイル読み込みの要の部分を抜粋したのが以下のソースコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//ファイルを読み込みモードでオープンする StreamReader sr = new StreamReader(filename, encode); //ファイルの終わりまでループ while (!sr.EndOfStream) { //ファイルから1行読み込み string line = sr.ReadLine(); } //ファイルを閉じる。 //Closeしないとファイルが掴まれたままになったりメモリーにゴミが残る sr.Close(); |
StreamReaderクラスのプロパティとして、 EndOfStream というのがあり、これを参照することでファイルをすべて読み終わったか否かが判断できます。
また、ファイルから1行取り出すメソッドとして ReadLine メソッドが用意されています。
StreamReaderのインスタンスを生成する際にファイル名と文字コードを引数として渡すことで、そのファイル名がその文字コードで参照できるようになります。
インスタンスを生成した時点でファイルがオープンされますので、処理が終わったら必ずCloseメソッドを読んで、ファイルのクローズをしなければなりません。
これをしないと、そのファイルがオープンされたままになったり、メモリにゴミが残ってしまいますので、必ずCloseメソッドを呼び出すようにして下さい。
では、このメソッドとそれを使った修正後のソースコードを見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
private void UxExecJoin_Click(object sender, EventArgs e) { //ヘッダ行数の取得 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int val)) ? val : 0; //結合対象CSVファイルの文字コード取得 var target_enc = Encoding.GetEncoding(uxTargeCharCode.Text); //出力CSVファイルの文字コード取得 var output_enc = Encoding.GetEncoding(uxOutputCharCode.Text); //結合対象フォルダにあるファイル名を全て読み込む var files = Directory.GetFiles(uxTargetFolder.Text); //ファイルがあれば最初の1ファイルを読んで出力ファイルに上書きする if (File.Exists(uxOutputFile.Text)) { File.Delete(uxOutputFile.Text); } //残りのファイルをループで処理 for (int cnt = 0; cnt < files.Length; cnt++) { //ヘッダスキップの数をcntが0か否かで決める var skip = (cnt == 0) ? 0 : head_cnt; //ファイルを改行で分割し、List形式に変換 var lines = ReadAllLines(files[cnt], skip,target_enc); //追加モードでファイル保存(既存ファイルに追加していく) File.AppendAllLines(uxOutputFile.Text, lines, output_enc); } } /// <summary> /// ヘッダスキップ機能付きファイル読み込み /// </summary> /// <param name="filename"></param> /// <param name="skip"></param> /// <param name="encode"></param> /// <returns></returns> private List<string> ReadAllLines(string filename,int skip,Encoding encode) { //読み込んだ内容を保持するListクラスを生成 List<string> lines = new List<string>(); //ファイルを読み込みモードでオープンする StreamReader sr = new StreamReader(filename, encode); //読み込んだ行数を数える行数カウンタ変数を定義 int cnt = 1; //ファイルの終わりまでループ while (!sr.EndOfStream) { //ファイルから1行読み込み string line = sr.ReadLine(); //読み込んだ行数がskip回数を超えたらListクラスに追加 if (skip < cnt) { lines.Add(line); } //行数カウンタを1プラスする cnt++; } //ファイルを閉じる。 //Closeしないとファイルが掴まれたままになったりメモリーにゴミが残る sr.Close(); //戻り値を返す return lines; } |
イベントハンドラの中身はずいぶんスッキリしたのではないかと思います。
forループの cnt 変数が0なら無視するヘッダ行数は0、cnt 変数が0以外なら無視するヘッダ行数に head_cntの値を使用することで、ヘッダの出力有無を切り替えています。
まとめ
いかがでしたでしょうか。
今回はいくつかのアルゴリズムについて実際の例と修正後のソースコードを紹介しました。
このように、同じことをするにしても様々な考え方(アルゴリズム)や記述ができます。
アルゴリズムはメモリ効率や処理速度、後々のメンテナンスのしやすさ等、色々な要素を考慮して決めていくものなので、状況によってはどの方法も正解になります。
趣味で行うDIYプログラミングでは、そこまで難しく考える必要はありませんので、自分が一番理解しやすい方法を選んでもらえればOKです。