2013年3月18日月曜日

ラムダ式を利用したリファクタリングの例 その2

こんな要求があったとします。

  • Shift-JISのCSVファイルを入力し、UTF-8のCSVファイルを出力する。
  • 入力したCSVの各カラムは、固定長で前後に空白が入る可能性があり、その空白は除去して出力する。
  • 各行の先頭のカラムはIDになっていて、特定のIDのみ出力対象とする。
この処理を実現するために、C#でこんなコードを書きました。(『特定のIDのみ出力』の処理は、単純化のため奇数のみ出力するようにしています。)

var sb = new StringBuilder();
string line;

using (var sr = new StreamReader(inFile, Encoding.GetEncoding("shift-jis")))
using (var sw = new StreamWriter(outFile))
{
  while ((line = sr.ReadLine()) != null)
  {
    var items = line.Split(',');
    sb.Length = 0;

    if (int.Parse(items[0]) % 2 != 0)
    {
      foreach (var item in items)
      {
        sb.Append(item.Trim()).Append(',');
      }
      sw.WriteLine(sb.ToString(0, sb.Length - 1));
    }
  }
}

まぁ、これで全く問題はないんですが、もうちょっと書きようはないか考えてみました。試行錯誤の結果、3つのstaticメソッドを作ることで、以下のような、割と好みのコードができました。

var sb = new StringBuilder();

using (var sr = new StreamReader(inFile, Encoding.GetEncoding("shift-jis")))
using (var sw = new StreamWriter(outFile))
{
  sw.WriteLines(sr.GetLineEnumerator()
    .Select(line => line.Split(','))
    .Where(items => int.Parse(items[0]) % 2 != 0)
    .Select(items => sb.Clear().AppendAll(
       items.Select(i => i.Trim() + ",")).ToString(0, sb.Length - 1)));
}

新たに作った3つのstaticメソッドは、以下のものすごくシンプルなものです。

1つめは、TextReaderクラスから各行ごとの読み込みを列挙可能なものにする拡張メソッド。
public static IEnumerable<string> GetLineEnumerator(this TextReader tr)
{
  string s;
  while ((s = tr.ReadLine()) != null)
  {
    yield return s;
  }
}

2つめは、TextWriterクラスに列挙可能な文字列配列等を渡して、各行ごとに出力する拡張メソッド。
public static void WriteLines(
          this TextWriter tw, IEnumerable<string> values)
{
  foreach (var line in values)
  {
    tw.WriteLine(line);
  }
}

3つめは、StringBuilderクラスに列挙可能な文字列配列等を渡して文字列を追記していく拡張メソッド。
public static StringBuilder AppendAll(
          this StringBuilder sb, IEnumerable<string> values)
{
  foreach (var item in values)
  {
    sb.Append(item);
  }
  return sb;
}

個人的にはこれで満足。

『でも結局ライン数もタイプ数も増えちゃってるじゃん。』…まったくもってその通りでございます。

でもでも、この3つのstaticメソッドを使いまわせるようにしておけば、かなり使い出のあるライブラリになるんすよ。

たとえば、テキストファイルを読み取って、文字列配列を作りたければこう書けるし、
using (var reader = new StreamReader(inFile))
{
  return reader.GetLineEnumerator().ToArray();
}

逆に文字列配列をテキストファイルにザーッと出力させたいときはこう書ける。
using (var writer = new StreamWriter(outFile))
{
  writer.WriteLines(lines);
}

あるいはたとえば、ネットワークストリームからの入力をファイルに落とすときはこう。
using (var reader = new StreamReader(netstream))
using (var writer = new StreamWriter(outFile))
{
  writer.WriteLines(reader.GetLineEnumerator());
}


こうやって、手持ちのライブラリを増やしていくと、よりコードを書くのが楽しく、ラクになると思うんす。

ちなみに、ここで一つ悩ましいのは、「TexReader.GetLineEnumerator」のメソッド名。「TextWriter.WriteLines」との対称性があったほうが分かりやすいんですが、「TextReader.ReadLines」としちゃうと、 その場でReadしてstring[]の戻りの型を予想しちゃうので、遅延実行のイメージがまるでなくなっちゃうため、あえて対称性を崩す形にしてます。が、これが正解かどうかはわからんですね。

もひとつちなみに、このコードはVS2008/.net 3.5だと失敗します。なんと、「StringBuilder.Clear」メソッドが.net 4.0以降にしかないのです。が、それならClearメソッドをStringBuilderクラスの拡張メソッドで作っちゃえば問題なしです。なんとかなります!


(2013/06/23 追記)
String.Joinを使えば、StringBuilderの拡張メソッドは不要でした。「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。」で別記事で書きました。

(2015/10/22 追記)
FileクラスにstaticメソッドのReadLines/ReadAllLines/WriteAllLinesというテキスト行操作メソッド群があったんですね。今日の今日まで知りませんでした。はずかしいいいい。
そんなわけで最新の記事は「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。(改)」に書き直しました。

0 件のコメント:

コメントを投稿