2013年6月23日日曜日

テキストファイルを行番号付きでコンソールに出力する処理をLINQで。

タイトルの通り、テキストファイルを読み取ってコンソールに出力。その時に行番号を先頭に表示させる処理を考えてみます。

SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。」と同様に、TextReader/TextWriterをLINQで使いやすくする拡張メソッドを用意しておきます。
public static IEnumerable<string> GetLineEnumerator(this TextReader tr)
{
  string s;
  while ((s = tr.ReadLine()) != null)
  {
    yield return s;
  }
}

public static void WriteLines(
          this TextWriter tw, IEnumerable<string> values)
{
  foreach (var line in values)
  {
    tw.WriteLine(line);
  }
}

で、出力時に行番号を付ける方法を考えます。LINQっぽく考えれば、行番号は1から順番の数字が必要となるので、Enumerable.Rangeが使えそう。そして、行番号と入力した各ラインをそれぞれ順次送り出せればよいので、Enumerable.Zipで纏めるのがよさそう。

ってことで、前述の2つの拡張メソッドも使い、こんなコードになりました。
static void Main(string[] args)
{
  using(var reader = new StreamReader(args[0]))
  {
    Console.Out.WriteLines(Enumerable.Range(1, int.MaxValue)
      .Zip(reader.GetLineEnumerator(), Tuple.Create)
      .Select(x => string.Format("{0}:{1}", x.Item1, x.Item2)));
  }
}

SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。

もともとは「ラムダ式を利用したリファクタリングの例 その2」で扱ったネタです。

こんな要求に対する処理を書いていました。
  • Shift-JISのCSVファイルを入力し、UTF-8のCSVファイルを出力する。
  • 入力したCSVの各カラムは、固定長で前後に空白が入る可能性があり、その空白は除去して出力する。
  • 各行の先頭のカラムはIDになっていて、特定のIDのみ出力対象とする。
以前書いた記事を見直してみて、String.Joinを使うことでよりシンプルなコードにできることに気がつきました。だいぶ今さらですが。

まずはやっぱり拡張メソッドを、でも今回は2つだけ用意して使います。

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);
  }
}

そのうえで、inFileが入力ファイルのパス、outFileが出力ファイルのパスとして、絞り込みの条件を「先頭カラムが奇数」とすると、以下のコードで要求が満たせますね。
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 => string.Join(",", items.Select(item => item.Trim()))));
}

2013年6月22日土曜日

個人的に無効にしたいFxCopとStyleCopのルール

多人数で開発する場合に特に有効と考えられるFxCopとStyleCopですが、個人的にはどうしても無効にしたいルールがいくつかあります。そこら辺を簡単に。

FxCop


FxCopでは、受け入れがたいものはあまり多くありません。でもプロジェクトの内容によりますが、以下は無効にすることが多いです。
Globalizationの2つについては、いずれも文字列の操作時なんかに余計なパラメータをセットすることを要求されます。これらはかなり細かい国際化をする場合じゃなければ必要になりませんし、必要無ければコードを汚しているようにしか感じられません。どうしても必要になったときにONにすればよいでしょ。

Designの2つについて、まずアセンブリの厳密名については、.net 1.x の頃ならともかく、GACに登録するようなものでなければ、厳密名は必要ないと個人的には認識しています。

また、CA1020は「アセンブリ内に同じ名前空間内にクラスやenum等の『型』を5つ以上含めるべき。」という内容ですが、使う側からすると、Usingをたくさん指定しなくちゃいけなくなって厄介なのはわかりますが、名前空間のUsing指定で拡張メソッドのON/OFFをしたりするので、名前空間の分割と統合は、型の数で制限すべきではないんじゃないかなぁ。ってことでOFFに。

StyleCop


StyleCopのほうは好みが出そう。ただ、あんまり無効にしちゃうと統一性が失われて意味がなくなっちゃうので、最低限に抑えておきたい。

デフォルト状態から、以下の項目をOFFにしています。
DocumentationRulesは、コードコメントの書き方のルール。さらにその中でも、ここで挙げたものは英語でコメントするときの問題で、日本語では関係ない(というか、無理)のでOFFにします。

OrderingRulesは記述の順番に関係するもの。ただ、SA1200は「名前空間の中でUsingを使え。」と言っています。つまり、こう書けと。
namespace Xxx.Yyy.Zzz
{
  using System;
  using System.Collections.Generic;
  using System.Linq;

  public class TheClass
  {
  }
}

はい?はじめて聞きました。こんなの。VisualStudioが吐き出すコードもこうなってないので、これは却下。

LayoutRulesのSA1503は、「中かっこは省略可能でも省略するな。」と言っています。つまり、こう書いちゃだめってこと。
if (condition())
    return false;

うーん。よく言われます。でもねぇ、コードが無意味に縦に伸びるの嫌なんですよ。インデントはされるわけですし、これは勘弁していただきたい。駄目かな?

SA1402は「一つのファイルには一つのクラスのみ含めよ。」と言っています。これもよく言われます。が、たとえばpublicなクラスでも、あるクラスにのみ依存するクラスってありますよね?大概は非常にちっさなクラス。この手のクラスにひとつひとつファイルを作っていると、ファイル数が多くなりすぎて却って参照性が悪くなると思うんです。…やっぱ駄目っすか?


それぞれ無効化しておきたいのはこれっくらいですねぇ。そのほかはMicrosoftさんのルールに従えるなー、というのが個人的な感想です。まぁ、これまでずっと、勉強するときのサンプルコードとかで散々Microsoftの書き方を見て、なんとなくでもわかりやすいと思って真似をしていた部分はあるんだと思います。多分、そのせい。

あー、もちろん、これ以外にその時々で「そんなの従えない!」ってことはあります。その場合は「SuppressMessage」属性をセットします。ちゃんと理由(Justification)を付けて。プロジェクトならできればコードレビューもしましょう。

2013年6月19日水曜日

FxCopとStyleCop まとめ

ADO.netのDataSetをLINQableに書くために (前段)」で書きましたが、「コンパイル(等)のタイミングで、指定したルールに適合していない場合はエラーや警告を出す」、コードの静的解析ツールとして、「FxCop」と「StyleCop」があります。

それぞれよく似た機能を持っているので混同されがちですが、何をしたいかによってどちらを選択するか、あるいは両方使うかが変わってくると思います。

なので、まずはそれぞれどのような特徴を持つのかをまとめてみました。

FxCopStyleCop
目的アセンブリが「クラスライブラリのデザインガイドライン」に適合し、利用者から見てAPIに一貫性と使いやすさが備わっているかを分析する。ソースコードが一定のルールに基づいて、統一的な記述がなされているかを分析する。(空白や改行の入れ方、括弧の位置、記述の順番など)
分析対象アセンブリ(IL:中間言語)C# ソースコード
VisualStudioバンドルPremium/Ultimateなし
VisualStudio統合Premium/Ultimateはビルド統合済み。
Standardは別途ツールをインストールすることで呼び出し可能になる。
Expressでは基本的にFxCopを単体で稼働させる。
Standard以上で可能。ただし、すべてのエディションでcsprojファイルの編集により、ビルドプロセス統合は可能。
カスタムルールの作成可能可能

つまるところ、FxCopは「ほかのアセンブリから呼び出されるアセンブリが、使いやすいものになっているか」どうかを分析し、使いにくいと思われる部分を警告を出して修正を促します。FxCopに怒られたところを直すと、確かに使いやすくなっているような気がします。

それに対してStyleCopは、「C#ソースコードが個々人によって差異が発生しないようにする」ために分析し、統一ルールからはみ出た表記には警告を出して修正を促します。

また、FxCopはアセンブリを対象に分析するため、言語はC#でもVB.netでもその他でもなんでもイケますが、StyleCopはソースコードが対象であり、C#に限定される。という違いもあります。

使ってみた印象としては、C#を使うプロジェクトで、それなりの人数が参加する場合はStyleCopを最初から適用する。さらに、いろんなところから参照されるアセンブリ(共通ロジックなど)はFxCopも適用する。というのがよろしいんではないかなー。と思っております。

ちなみに、ある程度開発が進んだ途中から、これらの静的解析を適用するのは、手間が大きすぎるので、可能な限り開発の着手前に決めておくべきでしょう。

なお、表には記載していませんが、StyleCopはオープンソース(Microsoft Public License:Ms-PL)に対して、FxCopはWindows SDKの一部であり、ソースは公開されていないという違いもあります。

それぞれ、インストール方法や使い方などについてはググってみてください。結構たくさん参考になるサイトが出てきます。

FxCopとStyleCop。もう少し続けます。

2013年6月14日金曜日

今週のモーニング

すべての流れをぶった切って、今週のモーニングについて。

相変わらず絶好調のモーニングですが、今は「カレチ」がとても悲しく、面白い。

「カレチ」は、JRの前身である「国鉄」において、諸々発生する問題を、現場の人たちの丁寧な、誠実な仕事で解決する。というお話で、鉄への愛と国鉄職員への愛で溢れている良作です。今週は最終章、5週連続掲載の2週目。

「国鉄」の「最後」の話。ということで、分割民営化が今描かれています。そして主人公の荻野は民営化前の、リストラの嵐に巻き込まれていきそうです。

今週の話を読んで思ったことをネタバレにならない程度にちょっとだけ言うと、現場で個人個人がどんだけ丁寧な仕事をしたって、組織というのは腐る。大きければ大きいほど腐りやすい。ということ。

それと、何話前だったかに舞台が現在に飛び、偏屈そうなジーさんになった荻野が登場していましたが、今の話があれに繋がるんだろうなぁ。と思うと…。やるせないなー。

楽しみにしていたお話が、あと3週で終わりを迎え、どうにもハッピーエンドは迎えられそうにない雰囲気。作者さんの力量もあり、とても面白いんですが切ないですね。

2013年6月13日木曜日

ADO.netのDataSetをLINQableに書くために (前段)

RDBMS利用時のアーキテクチャの選択方針」では、データセットを使うに当たっては以下をおススメしていました。
型無しデータセットを使う場合は、コードのメンテナンシビリティを保つために、以下の2つを守ることがおススメです。

ただし、 事はそう簡単ではありません。プロジェクトで何人かで開発を進めるなら、.net経験者を集めます。そしてその開発者は大抵過去にデータセットを使ったことがあるはずです。

そしてプロジェクトの方針としてこれらのことを掲げても、故意ではなくてもつい使いなれたメソッドやプロパティを使っちゃいます。そして、それをあとから直すのはとても大変です。つまり、ある程度強制力のある手法が必要になってきます。

このように、「開発にあたってのルールを強制する」方法を考えた時、2つのツールが有用です。それは、「FxCop」と「StyleCop」です。それぞれ「コンパイル(等)のタイミングで、指定したルールに適合していない場合はエラーや警告を出す」という機能を持ちます。この2つは似た機能を持っていますが、目的もアプローチも異なります。

この次のエントリでは、FxCopとStyleCopについてちょっと書いてみたいと思っています。

2013年6月9日日曜日

DbCommandからのレコード取得をLINQableにする拡張メソッド

RDBMS利用時のアーキテクチャの選択方針」では、バッチ処理では生ADO.netが相性が良いと書きました。ただこのクラス群はさすがに古くて、あまり積極的に使いたくないなー。と思ってしまうところがあります。

特に、SQLを実行して結果を取得しない「DbCommand.ExecuteNonQuery」や単一の値を求める「DbCommand.ExecuteScalar」であればともかく、SELECTして複数行にわたって処理する場合、「DbCommand.ExecuteReader」を使う必要がありますが、どう頑張ったってこんなコードになっちゃう。
using (var conn = new SqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
{
  conn.Open();
  cmd.CommandText = selectCmd;

  using (var reader = cmd.ExecuteReader())
  {
    while (reader.Read())
    {
      xxx = reader[columnName];
      // いろいろ処理
    }
  }
}
んむー。せめてwhileじゃなくてforeachを使いたい。できればLINQに繋げたい。そのためには、SELECTの結果のレコードを、IEnumerable<T>で取得できるようなメソッドを用意しておきたい。そこで、DbCommandクラスにこんな拡張メソッドを用意してみます。なお、わかりやすくするためにパラメータのNULLチェックはなしで。
public static IEnumerable<IDataRecord> ExecuteQuery(this DbCommand command)
{
  return ExecuteQuery(command, dr => dr);
}

public static IEnumerable<T> ExecuteQuery<T>(
    this DbCommand command, Func<IDataRecord, T> mapper)
{
  using (var reader = command.ExecuteReader())
  {
    while (reader.Read())
    {
       yield return mapper(reader);
    }
  }
}
「ExecuteQuery」という名前の2つの拡張メソッドを作ります。一つはIEnumerable<IDataRecord>を返し、もう一方は任意の変換関数を用意して、IEnumerbale<T>を返します。ちなみに「IDataRecord」はDbCommand.ExecuteReaderメソッドが返す、DbDataReader抽象クラスの派生クラスで実装されます。

これを使うと多少ましになります。例としてなんか適当なバッチ処理を考えてみましょうか。

SQLServerのあるテーブルをCSVファイルにエクスポートします。その際に「exported」カラムが「0」のレコードのみを対象とします。また、エクスポート実行後、対象レコードの「exported」カラムを「1」に、「exportAt」カラムに現在日時をセットします。

こんなありがちな処理を先のDbCommand拡張メソッドを使って書いてみると、こんなコードになります。
const string connectionString =
  @"Data Source=.\SQLEXPRESS;Initial Catalog=Test;Integrated Security=True";
const string selectCmd =
  "select id, uid, insertAt, qty from TestTable_1 where exported=0";
const string updateCmd =
  "update TestTable_1 set exported=1, exportAt=getdate() where id in ({0})";

static void Main(string[] args)
{
  using (var conn = new SqlConnection(connectionString))
  {
    var ids = new List<int>();

    using (var cmd = conn.CreateCommand())
    using (var csv = new StreamWriter("output.csv"))
    {
      conn.Open();
      cmd.CommandText = selectCmd;

      foreach (var r in cmd.ExecuteQuery())
      {
        ids.Add(r.GetInt32(0));
        csv.WriteLine(string.Format("{0},{1},{2}", r[1], r[2], r[3]));
      }
    }

    if (ids.Count > 0)
    {
      using (var cmd = conn.CreateCommand())
      {
         cmd.CommandText = string.Format(updateCmd, string.Join(",", ids));
         cmd.ExecuteNonQuery();
      }
    }
  }
}
意外と見やすいコードが書けるんじゃないかと思ってるんですが、どうでしょ?

2013年6月8日土曜日

あっというまに

前回のエントリ立てから2週間以上が経過。しかもその間、一回もおうちのPC立ち上げて無いや。ちょっと反省。

どんな内容でもいいので、週に一回はエントリ立てたいという希望を持っていこうと思います。