1つの処理を実現する際、様々な手順(アルゴリズム)が考えられますが、前回はその一例として異なるアルゴリズムを紹介しました。
今回は、プログラムの書き方にも様々なパターンがあるという事を紹介したいと思います。
Google検索すると、同じ言語で同じ事をしているにもかかわらず、ずいぶん書き方が違う事が有ります。
その一例としてCSV分割ツールをリライトしていみました。
ソースコード
リライトした全体のプロジェクトは下記からダウンロード可能です。
ソースコード全体は以下の通りです。
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.IO; namespace CsvDivider { public partial class MainForm : Form { /// <summary> /// コンストラクタ /// </summary> public MainForm() { InitializeComponent(); } /// <summary> /// 分割実行ボタンのクリックイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void UxExecDivide_Click(object sender, EventArgs e) { //ヘッダ行数を取得する。数値変換できなかったら0を代入 var head_cnt = (int.TryParse(uxHeaderCount.Text, out int var1)) ? var1 : 0; //CSVファイルの読み込み List<string> lines = File.ReadAllLines(uxCsvFile.Text,Encoding.GetEncoding("shift-jis")).ToList(); //ヘッダー行を別変数にコピー var header = Enumerable.Range(0, head_cnt).Select(i => lines[i]).ToArray(); //読み込んだCSVからヘッダー行数だけ削除 lines = Enumerable.Range(head_cnt, lines.Count - head_cnt).Select(i => lines[i]).ToList(); //分割行数の取得する。数値に変換できなければ0を代入 var divid_cnt = (int.TryParse(uxDivideCount.Text, out int var2)) ? var2 : 0; //分割保存関数の呼び出し DivideSave(uxOutputFolder.Text, uxCsvFile.Text, header, lines.ToArray(), divid_cnt); } /// <summary> /// 指定行数で分割しながらファイル出力 /// </summary> /// <param name="Folder">出力先フォルダ名</param> /// <param name="fileName">CSVファイル名</param> /// <param name="headers">CSVヘッダ</param> /// <param name="lines">CSVデータ</param> /// <param name="divideCnt"></param> private void DivideSave(string Folder,string fileName,string[] headers,string[] lines,int divideCnt) { divideCnt--; var cnt = divideCnt; //行数カウント変数 var file_no = 0; //ファイル連番 var buff = new List<string>(); //一時保存用LIST変数 //全ての行数をループ処理 foreach (var line in lines) { //一時保存用LIST変数に行を追加 buff.Add(line); //行数カウント変数が分割行数を超えていないかチェック if (cnt -- <= 0) { //ヘッダとデータをファイルに保存 save(file_no ++ , headers, buff); //行数カウント変数をクリア cnt = divideCnt; //一時保存用LIST変数をクリア buff.Clear(); } } //ファイル保存されていないデータがあるかチェック if(cnt > 0) { //ヘッダとデータをファイルに保存 save(file_no, headers, buff); } void save(int p_no, string[] p_headers, List<string> p_lines) { //出力パス(フォルダ名+ファイル名+連番+拡張子)を作成 var path = string.Format(Path.Combine(Folder, Path.GetFileNameWithoutExtension(fileName) + "{0}" + Path.GetExtension(fileName)), p_no); //先頭にヘッダを挿入 p_lines.InsertRange(0, p_headers); //ファイル保存 File.WriteAllLines(path, p_lines.ToArray(), Encoding.GetEncoding("shift-jis")); } } /// <summary> /// CSVファイル選択ボタンのクリックイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void UxSelectFile_Click(object sender, EventArgs e) { //フォルダ選択ダイアログのインスタンスを生成 var dialog = new OpenFileDialog() { FileName = uxOutputFolder.Text }; //フォルダ選択ダイアログで選択したフォルダを画面項目に表示 uxCsvFile.Text = (dialog.ShowDialog() == DialogResult.OK) ? dialog.FileName : uxCsvFile.Text; } /// <summary> /// 出力フォルダ選択ボタンのクリックイベント /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void UxSelectFolder_Click(object sender, EventArgs e) { //フォルダ選択ダイアログのインスタンスを生成 var folder = new FolderBrowserDialog() { SelectedPath = uxOutputFolder.Text }; //フォルダ選択ダイアログで選択したフォルダを画面項目に表示 uxOutputFolder.Text = (folder.ShowDialog() == DialogResult.OK) ? folder.SelectedPath : uxOutputFolder.Text; } } } |
最初のソースと見比べないと良く分からないかもしれませんね。
特長としては、Linq、ラムダ式、ローカル関数という2つの技術を使ってるという点です。
Linqとは
Linqは、Language Integrated Query の略で、マイクロソフトのサイトでは「統合言語クエリ」とも書かれています。
簡単に言うと、配列などのデータに対して、データベースで使われているSQLの様な書き方で、抽出や加工を行えるようにする拡張機能という解釈になろうかと思います。
Listや配列形式のデータに対してデータを加工したり、特定の条件に合致するデータだけを抜き出したりする場合、通常は forループを使います。
例えば、下記は先頭が “TEL:”から始まる行を抜き出すサンプルですが、Linqで書くと1行で書くことが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//通常のForeachを使った書き方 List<string> result = new List<string>(); foreach(string line in lines) { if(line.StartsWith("TEL:")) { result.Add(line); } } //Linqを使った書き方 List<string> result = lines.Where(i=>i.StartsWith("TEL:")).ToList(); |
ソースコード量が減るだけでなく、1行に凝縮されることで、中身が直感的に分かりやすくなるという特徴があります。
Linqはネスト(何段にも重ねて記述)が可能であり、抽出した結果から更に加工したり、別の条件で絞り込む事もできるため、やり過ぎると逆に可読性が悪化することもありますが、適度な利用は可読性向上に役立ちます。
ラムダ式とは
「記述した部分だけで有効な名前の無い関数」という表現が分かりやすいかと思います。
通常、関数(メソッド)はpublicやprivate、戻り値の型、関数名、引数の型、引数などを定義することで、初めて使える様になります。
ラムダ式は、このような限定的な場所で用いる関数の定義を簡略化したもので、
(型 引数名、型 引数名、・・・) => { 処理1; 処理2; ・・・ return 戻り値; }
という風に記述します。
ラムダ式はLinqやイベントハンドラで使われることが多く、先ほどのLinqで言うと、青い斜め文字の部分がラムダ式になります。
List result = lines.Where(i=>i.StartsWith(“TEL:”)).ToList();
Linqで使われているのと少し書き方が違いますね。
本来は下記の様に書くのが正しいような・・・
List result = lines.Where((string i)=>{ return i.StartsWith(“TEL:”);}).ToList();
そうなのですが、引数が1つで引数の型が推測できる場合は、引数名以外は省略可能です。
また、{ }内の処理が1つの場合も { } と return は省略可能です。
以上のことから、Linqで使用されている場合は、省略して書くことが一般的になっています。
ローカル関数とは
ローカル関数は、関数(メソッド)の中に記述するローカルな関数です。
通常の関数(メソッド)は、privateならクラス内のメソッドから、publicならクラス外から呼び出されますが、ローカル関数は定義した関数(メソッド)以外から使用することが出来ません。
また、関数(メソッド)内で定義した変数は、そのままローカル関数内で参照可能な点が異なります。
1 2 3 4 5 6 7 8 9 10 11 |
private void calc(int a,int b) { var pi = 3.1415926; return keisu(a,b); //ローカル関数 int keisu(int p1,p2) { pi * p1 / p2; // pi が参照できる } } |
主な変更点
それでは、主な変更点を見てみましょう。
上がリライト前の記述、下がリライト後の記述になります。
画面からの数値変換に三項演算子
1 2 3 4 5 6 7 8 9 10 |
//ヘッダ行数を取得する。数値変換できなかったら0を代入(リライト前) if (int.TryParse(uxHeaderCount.Text, out int head_cnt) == false) { head_cnt = 0; } //ヘッダ行数を取得する。数値変換できなかったら0を代入(リライト後) var head_cnt = (int.TryParse(uxHeaderCount.Text, out int var1)) ? var1 : 0; |
前は3行でしたが、今回は3項演算子を使って1行になっています。
3項演算子は、条件式が true なら値1を、false なら値2を返す演算子です。
(条件式)? 値1 : 値2
int.TrpParse メソッドは、引数に渡された文字列が数値に変換できれば trueを返しますので、false の場合は初期値である 0 を返すようにしています。
ループ処理のLinq化
次は、ヘッダ行を別変数にコピーする部分です。
1 2 3 4 5 6 7 8 9 |
//ヘッダー行を別変数にコピー(リライト前) for(int i = 0;i < head_cnt; i ++) { headers[i] = lines[i]; } //ヘッダー行を別変数にコピー(リライト後) var header = Enumerable.Range(0, head_cnt).Select(i => lines[i]).ToArray(); |
Linqとラムダ式を使って、1行になりました。
末尾に ToArray() メソッドを呼び出していますが、これは結果を文字列配列に変換するためのメソッドです。
DivideSaveにローカル関数
行数をカウントし、分割行数に達したらファイルに保存する処理は、ループ内とループ終了後の2か所ありました。
リライト後は、この部分をローカル関数化しています。
これに伴い、ファイル名を生成する部分も、ローカル関数内に収めました。
1 2 3 4 5 6 7 8 9 10 11 |
void save(int p_no, string[] p_headers, List<string> p_lines) { //出力パス(フォルダ名+ファイル名+連番+拡張子)を作成 var path = string.Format(Path.Combine(Folder, Path.GetFileNameWithoutExtension(fileName) + "{0}" + Path.GetExtension(fileName)), p_no); //先頭にヘッダを挿入 p_lines.InsertRange(0, p_headers); //ファイル保存 File.WriteAllLines(path, p_lines.ToArray(), Encoding.GetEncoding("shift-jis")); } |
ダイアログ表示の簡略化
ダイアログ表示の部分は、クラスのインスタンス生成と同時にプロパティに初期値をセットするとともに、ダイアログからの戻り値を三項演算子で処理することにより、ソースコードを半分にしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//リライト前 //ファイルオープンダイアログのインスタンスを生成 OpenFileDialog dialog = new OpenFileDialog(); //ファイルオープンダイアログのファイル名に画面項目の値を代入 dialog.FileName = uxCsvFile.Text; //ファイルオープンダイアログの表示と戻り値のチェック if(dialog.ShowDialog() == DialogResult.OK) { //ファイルオープンダイアログで指定したファイル名を画面項目に表示 uxCsvFile.Text = dialog.FileName; } //フォルダ選択ダイアログのインスタンスを生成 var dialog = new OpenFileDialog() { FileName = uxOutputFolder.Text }; //リライト後 //フォルダ選択ダイアログで選択したフォルダを画面項目に表示 uxCsvFile.Text = (dialog.ShowDialog() == DialogResult.OK) ? dialog.FileName : uxCsvFile.Text; |
クラスのインスタンス生成時、プロパティに初期値を設定するには、次の様に記述します。
クラス名 変数名 = new クラス名(){ プロパティ1 = 初期値1,プロパティ2 = 初期値2,・・・};
今回は、以下の様に FileNameプロパティにuxOutputFolderコントロールのTextプロパティの値を設定しています。
1 |
var dialog = new OpenFileDialog() { FileName = uxOutputFolder.Text }; |
まとめ
同じアルゴリズムですが、ちょっとした書き方のテクニックを使う事で、ソースコード量が減るとともに、プログラムの読みやすさも向上しました。
これ以外にもリライト可能な部分はありますが、それは次の機会に譲りたいと思います。
今回の記述方法はよく使うものなので、どんどん使って自分のものにしていきましょう。