ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

2016年8月1日月曜日

IDisposable 汎用クラスの実装

ものすごく抽象的になりますが、おおよそこんなコードがありました。

var a = new A();
a.Do();

var b = new B(a);
b.Do();

// 後片付け
b.Cleanup();
a.Cleanup();

ところが実際には、A.DoにしろB.Doにしろ、省略しているそれ以外のところでも、例外がスローされる可能性がありました。コンストラクト済みのA、Bの両オブジェクトは、Cleanupメソッドを呼ぶ必要があり、例外がスローされた場合も正しく処理されるためには、以下のようにコードを修正する必要がありました。

A a = null;
B b = null;

try
{
  a = new A();
  a.Do();

  b = new B(a);
  b.Do();
}
finally
{
  if (b != null)
    b.Cleanup();

  if (a != null)
    a.Cleanup();
}

結果として、A、Bいずれも宣言と同時に初期化ができなくなり、finally句内の後片付けも、構築済みかどうかのチェックが必要になってしまいました。

実際、この手のコードはたまに見かけます。正しく書くとこうなってしまうケース。でも、正直カッコワルいよね。ちなみに、このケースならA,Bの両クラスがIDisposableを実装して、DisposeでCleanupできれば問題ないんだけど、そうもいかないケースも結構多いですよね。

で、考えてみたのが「それなら、IDisposableの汎用クラスを作ってしまえば良いんじゃないか」ということ。試しに作ってみたのは、こんなどシンプルなクラス。クラス名は「Disposeするヒト」なので、「Disposer」クラス。

public class Disposer : IDisposable
{
  private readonly Action disposer;

  public Disposer(Action disposer)
  {
    this.disposer = disposer;
  }

  public void Dispose()
  {
    disposer();
  }
}

これを作っておくことで、先のコードはこう変わります。

var a = new A();
using (new Disposer(() => a.Cleanup()))
{ 
  a.Do();

  var b = new B(a);
  using (new Disposer(() => b.Cleanup()))
  {
    b.Do();
  }
}

usingのスコープを抜けたときに行う処理を、あらかじめ定義しておくイメージでしょうか。ちなみに、using句でnewされたオブジェクトが特に参照されないのであれば、変数に代入しなくてもちゃんと動くんですね。初めて知った。

2015年10月22日木曜日

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

もともとは「ラムダ式を利用したリファクタリングの例 その2」で扱った後、さらに「SJISのCSVファイルを各カラムの操作と条件による絞り込みを行い、UTF-8のCSVファイルを出力する処理をLINQで。」で修正版を示したネタです。改二ですな。

こんな要求に対する処理を書いていました。
  • Shift-JISのCSVファイルを入力し、UTF-8のCSVファイルを出力する。
  • 入力したCSVの各カラムは、固定長で前後に空白が入る可能性があり、その空白は除去して出力する。
  • 各行の先頭のカラムはIDになっていて、特定のIDのみ出力対象とする。
inFileが入力ファイルのパス、outFileが出力ファイルのパスとして、絞り込みの条件を「先頭カラムが奇数」とすると、以下のコードで拡張メソッドとか作らなくても要求が満たせてしまいますね。

File.WriteAllLines(
  outFile,
  File.ReadLines(inFile, Encoding.GetEncoding("shift-jis"))
    .Select(line => line.Split(','))
    .Where(items => int.Parse(items[0]) % 2 != 0)
    .Select(items => string.Join(",", items.Select(item => item.Trim()))));

  • inFileからShift-JISで各行を読み込み、
  • ',' で分割し、
  • 先頭カラムが奇数の行について、
  • 各カラムの前後の空白を除去した上で再度 ',' で結合し、
  • outFileに書き込む。

という処理になります。日本語とほぼ一対一にコードが対応してます。ちなみに、.net 2.0相当でコードを書くとこんな感じ。

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

雲泥の差がありますねぇ…。

2015年7月6日月曜日

C#でLEFT OUTER JOIN (左外部結合)

LINQを使って、「LEFT OUTER JOIN」をしたかった。普通のJoinだとINNER JOINなので、少し工夫する必要がありそう。ググってみると、以下のようなコードが一般的なようだ。

var query = from person in people
  join pet in pets on person equals pet.Owner into gj
  from subpet in gj.DefaultIfEmpty()
  select new
  {
    person.FirstName, 
    PetName = (subpet == null ? String.Empty : subpet.Name)
  };

ちなみにこれは、MSDNの「方法 : 左外部結合を実行する (C# プログラミング ガイド)」から。

大体どこを見てもこんな感じで、ほとんど左外部結合をしたいときの慣用句(イディオム)みたいなもののよう。とはいえ、このコードを見て「あ、左外部結合させたいんだな」と思える人がどれくらいいるか。要は、コードから意図が解りづらいので、あまり好みじゃないなぁ。と。

もう少し、コードを見て左外部結合であることが解るように、拡張メソッドを作ってみた。こんなの。

public static class EnumerableEx
{
  public static IEnumerable<TResult> OuterJoin<TOuter, TInner, TKey, TResult>
    (this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector,
    TInner innerDefaultValue)
  {
    return outer
      .GroupJoin(
        inner,
        outerKeySelector,
        innerKeySelector,
        (o, i) => new { Out = o, Ins = i.DefaultIfEmpty(innerDefaultValue) })
      .SelectMany(g => g.Ins, (g, i) => resultSelector(g.Out, i));
  }
}

Enumerable.Joinメソッドと比べると、最後のパラメータが余計についてます。これはOuterの要素に対して合致するInnerの要素がなかった時の代替要素を指定するもの。Null Objectだと思っておけばよいと思います。

あと、名前は長いのを嫌い、「OuterJoin」としています。タイプパラメータからも、左が外なのは明らかなので、特に問題はないと思っています。

ちなみに、これをライブラリ的に用意するなら、GroupJoinするときにEqautityComparerを指定するパターン、それと、DefaultIfEmptyでTInnerのデフォルト値を自動的に使うパターン、その組み合わせで4つのオーバーロードを、用意しておくのがよいと思います。

さて、テストします。

static void Main(string[] args)
{
  var products = new[] 
  {
    new { ProductId = 1, Name = "えんぴつ" },
    new { ProductId = 2, Name = "けしごむ" },
    new { ProductId = 3, Name = "コンパス" },
    new { ProductId = 4, Name = "クレヨン" },
  };

  var sales = new[]
  {
    new { SaleId = 1, ProductId = 1, Buyer = "○○商会", Quantity = 2 },
    new { SaleId = 2, ProductId = 1, Buyer = "××文具店", Quantity = 4 },
    new { SaleId = 3, ProductId = 2, Buyer = "△屋", Quantity = 3 },
    new { SaleId = 4, ProductId = 4, Buyer = "○○商会", Quantity = 1 },
    new { SaleId = 5, ProductId = 5, Buyer = "○○商会", Quantity = 1 },
  };

  var nosaled
    = new { SaleId = 0, ProductId = 0, Buyer = "(no sales)", Quantity = 0 };

  var salesInfo = products.OuterJoin(
    sales, 
    p => p.ProductId, 
    s => s.ProductId, 
    (p, s) => new { p.Name, s.Buyer, s.Quantity }, 
    nosaled);

  foreach (var i in salesInfo)
  {
    Console.WriteLine("{0}, {1}, {2}", i.Name, i.Buyer, i.Quantity);
  }
}

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
コンパス, (no sales), 0
クレヨン, ○○商会, 1

…右外部結合?RIGHT OUTER JOINか。僕自身使ったことないですが、必要なら右左を入れ替えてあげればいいはず。上のコードの20行目から28行目を、こんな感じに置き換えてみる。
var noproduct = new { ProductId = 0, Name = "(no item)" };

var salesInfo = sales.OuterJoin(
  products,
  s => s.ProductId,
  p => p.ProductId,
  (s, p) => new { p.Name, s.Buyer, s.Quantity },
  noproduct);

結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
クレヨン, ○○商会, 1
(no item), ○○商会, 1

うん。いんじゃないかな?
…完全外部結合?…FULL OUTER JOIN…。必要?それ。

使い道が解らなくてモチベーションゼロだけど、要するに左外部結合+右のみに存在するレコードを、編集してUnionすればいいんじゃないかな?多分。

2015年7月3日金曜日

GroupByとToLookup

ここしばらく、C#関連記事ではGroupByを使ったものが続いています。

ところで、.net Frameworkには、GroupByと非常によく似た「ToLookup」というEnumerable拡張メソッドがあります。大体名前のイメージで、GroupByは遅延実行で、ToLookupは即時実行だと思い、基本的に使い分けていました。

もう少し調べてみると、ToLookupはその名の通りルックアップテーブルを作るメソッドで、戻されるLookupクラスは、キーからのルックアップを機能として有している。と。

ところがふと、過去にGroupByを使っていて、ToLookupを使ったほうがよいケースがあったりするんじゃないかと不安になってきまして。たとえば何も考えずにGroupByを使っていた箇所で、ToLookupを使ったほうが実はよかった。とかいうケースがあったらいやだなぁ。と。

なので、とりあえず処理時間を計ってみました。使ったのはこんなコード。

static void Main(string[] args)
{
  var rnd = new Random(DateTime.Now.Millisecond);
  var items = Enumerable.Range(0, 10000000).Select(_ => new
  {
    Key1 = rnd.Next(10),
    Key2 = rnd.Next(100),
    Value = rnd.Next(10000)
  });

  var sw = new Stopwatch();
  sw.Start();

  var dic = items.GroupBy(i => new { i.Key1, i.Key2 })
    .ToDictionary(g => g.Key, g => g.First().Value);

  sw.Stop();

  Console.WriteLine("Elapsed:{0}msec", sw.ElapsedMilliseconds);
}

先日の『ToDictionaryで重複のない辞書を作る』と同様に、コレクションからキー重複を排除した辞書を作る処理にGroupByを使ってみて、単純にGroupBy→ToLookupに置き換えたときにどれくらい処理速度の差があるか?ほんの少しGroupByが早いんじゃないかな?と予想。

結果は、
GroupBy … 6155msec。
ToLookup … 6081msec。
となり、予想に反してほんの誤差レベルとはいえGroupByのほうが遅い結果に。なんでだろう?と思ってソースコードを調べてみた。

GroupByは大体こんな感じ。

public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return new GroupedEnumerable<TSource, TKey, TSource>(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

internal class GroupedEnumerable<TSource, TKey, TElement>
  : IEnumerable<IGrouping<TKey, TElement>>
{
  public IEnumerator<IGrouping<TKey, TElement>> GetEnumerator()
  {
    return Lookup<TKey, TElement>.Create<TSource>(
      source, keySelector, elementSelector, comparer).GetEnumerator();
  }
}

それに対して、ToLookupはこんな感じ。

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  return Lookup<TKey, TSource>.Create(
    source, keySelector, IdentityFunction<TSource>.Instance, null);
}

あー。これだとほぼ即時実行か遅延実行かの違いしかないですねぇ。これならToLookupが若干速いのも納得できる。

というか、そうするとGroupByの存在意義が解らなくなったので、ちょっと調べてみたら、Stackoverflowに「Why are ToLookup and GroupBy different?」こんな質問があって、その回答が以下。
What happens when you call ToLookup on an object representing a remote database table with a billion rows in it?
なるほど。例えばデータソースがObjectではなくDatabaseだったりしたときに、即時実行しないで済むならそうしたい。LINQのメソッドはデータソースを問わないわけだから、基本的にはGroupByを使い、明示的に即時実行にしたい場合や、実行後のルックアップが必要なら、ToLookupを使うべし。と、解釈して納得できた。

気にしなければならない速度差では全くないですしね。

2015年6月22日月曜日

ToDictionaryで重複のない辞書を作る

シンプルなTipsをひとつ。

LINQのToDictionaryメソッドは、コレクションから辞書を簡単に作ってくれる、使用頻度の高い便利なメソッドです。ただ、指定したキーセレクタの結果、キーが重複していると例外(ArgumentException)をスローします。例えば、以下のようなコード。

var collection = new [] { "One", "Two", "Three", "Four" };
var dic = collection.ToDictionary(c => c.First());

この例では、文字列の最初の文字をキーにするので、キー'T'が重複します。しかし、キーに対して最初に現れた値を登録するような、単純なルールで重複を排除して辞書を作りたいケースがあります。

さて、どう解決しましょうか。ToDictionaryのメソッドで、重複を排除するようなオーバーロードがあれば簡単ですが、ないですね。なので、こんなコードではどうでしょうか?

var collection = new [] { "One", "Two", "Three", "Four" };
var dic = collection.GroupBy(c => c.First())
    .ToDictionary(g => g.Key, g => g.First());

ToDictionaryの前に、一旦最初の文字でグループ化しておいて、そのグループキーと最初の値を辞書のエントリとしてみました。

2015年1月14日水曜日

Import時の重複チェックにLINQ(GroupBy)を利用する

外部のシステムからデータをインポートする機能は、多くのシステムで必要になってきます。CSVなりXMLなりの標準化された形式で送られたデータを、一括でシステムに取り込む機能です。

この場合、普通の入力とは異なり、一括で処理するゆえのメンドクササがありますよね。

たとえば、ユーザ情報を外部システムからインポートする場合。想定するケースによりますが、ユーザIDがインポートデータ中に重複して存在する可能性があったりすると、せめて取り込みの事前のチェックを行っておきたいケースもあると思います。が、これが結構面倒。

これをGroupByメソッドを活用して、なるべくシンプルに記述してみます。想定する前提はこんな感じだとします。
  • インポートするユーザ情報データは、すでにオブジェクトへのマッピングがされており、コレクション化されている前提。
  • ユーザ情報データは「UserInfo」クラスに格納されており、ユーザIDは文字列のメンバUserIdとして存在する。
  • ユーザ情報データのコレクションを入力として、ユーザIDの重複チェックを行う静的メソッド「ValidateNotDuplicated」を作成する。
  • このメソッドは、ユーザIDの重複があった場合、コンソールにエラーメッセージを出力し、メソッドの戻り値はfalseを返す。重複がなければそのままtrueを返す。
  • エラーメッセージには、重複しているユーザIDを表示する。複数のユーザIDが重複していた場合は、そのすべてを表示する。
このような処理を書くと、こんな感じになりますね。

public static bool ValidateNotDuplicated(IEnumerable<UserInfo> users)
{
  var dupusers = users.GroupBy(u => u.UserId)
    .Where(g => g.Count() > 1)
    .Select(g => g.Key)
    .ToArray();

  if (dupusers.Length > 0)
  {
    Console.WriteLine("User:{0} was duplicated.", string.Join(",", dupusers);
    return false;
  }

  return true;
}

つまり、「コレクションをUserIdでグループ化し(GroupBy)」、「そのうち2つ以上存在するものを選び出し(Where)」、「キーとなっているUserIdのみを対象として(Select)」、「配列に変換する(ToArray)」。という処理を行っています。

ちょっとだけバリエーションを考えて見ましょうか。たとえば、
  • 「UserInfo」クラスには、データが更新された時間を示す「Updated」メンバ(DateTime型)も持っている。
  • このユーザ情報データのコレクションを入力として、ユーザIDが重複していた場合、「Updated」が最新のもの以外を取り除いたコレクションを返す、静的メソッド「SelectLatest」を作成する。
この場合ならもっとシンプルで、

public static IEnumerable<UserInfo> SelectLatest(IEnumerable<UserInfo> users)
{
  return users.GroupBy(u => u.UserId)
    .Select(g => g.OrderByDescending(u => u.Updated).First());
}
これだけになりますね。


2015年1月12日月曜日

Enumerable.GroupByメソッドの威力

2015年も明けて間もなく2週間が経とうとしています。

おそくなりましたが、今年もよろしくお願いします。

今年の目標は「去年のエントリ数を超える」こと。2014年は5本だったようなので、達成できそうな目標にしてみました。

一発目のエントリは、「GroupBy」をテーマにしてみます。非常に強力なLINQのメソッドですが、なかなか使う機会が少ない人も多いのではないかと思います。

「GroupBy」メソッドは、SQLを触ったことのある人なら「group by」句をイメージしてもらうとほぼ間違いなしで、つまりは「コレクション内の各要素を、ある条件でグループ分けする」メソッドです。ぱっと見面倒そうですが、使い方はいたってシンプルです。

サンプルを書いてみましょう。

  • データメンバとして、日付(Date)と売上額(Amount)を持つSalesクラスがあります。
  • Salesクラスは、ある指定期間の日別の売上額を列挙するEnumerate静的メソッドを持ちます。
  • ここで、上記Salesクラスを使い、ある年(ここでは2014年)の曜日別の売り上げの総合計、平均値、中間値を算出します。
この処理は以下のコードになります。

static void ShowGroupedAmounts()
{
  var from = new DateTime(2014, 1, 1);
  var to = new DateTime(2014, 12, 31);
  var group = Sales.Enumerate(from, to)
    .GroupBy(s => s.Date.DayOfWeek)
    .OrderBy(g => g.Key);

  Console.WriteLine("DayOfWeak,Total Amount,Average,Median");

  foreach (var g in group)
  {
    var cnt = g.Count();

    Console.WriteLine(
      "{0},{1},{2},{3}",
      g.Key,
      g.Sum(s => s.Amount),
      Math.Round(g.Average(s => s.Amount), 1),
      g.OrderBy(s => s.Amount).Skip(cnt / 2).First().Amount);
  }
}

非常にシンプルなコードで、キー(ここでは曜日)ごとにグループ化できていると思います。なお、GroupByメソッドの戻り値は以下の形態になります。

IEnumerable<IGrouping<DayOfWeak, Sales>>

IGroupingは、IEnumerable<T>にキー情報を加えたもの。と考えればOKです。

2014年10月27日月曜日

KeyedCollectionクラス

.netでコレクションというと、ほとんどのケースでListとDictionaryがあれば事足りてしまう印象があります。

とはいえ、標準のクラスライブラリには、かなりたくさんのバリエーションがあり、多くはほとんど使われていないんじゃないかと思います。(System.Collection名前空間に属するものは使わないほうがよいですが。)

そんな中で、System.Collection.ObjectModel名前空間に属するものに、たまに使いたくなるものがあったりします。

そのうちの一つに、「KeyedCollection」クラスがあります。これは、ListとDictionaryの両方の特徴を持つようなクラスになっていて、

  • Dictionaryのようにキーで要素にアクセスできる。
  • Listのように追加/挿入時の順番が保持され、Indexでアクセスできる。
  • 要素の一部がキーとして扱われる。
  • キーの重複は不可。
といった特徴を有します。なので、たとえばEntityの中にIDのような一意な値を持っていて、そのキーでアクセスするようなケースで有用です。さらに順番を保持させたいなら積極的に使うべきクラスかも。

ただ、大変惜しむらくは、KeyedCollectionクラスは抽象仮想クラスになっていて、中に収めるEntityごとにクラスを派生させて使う前提になってしまっています。因みに、派生クラスでオーバーロードするメソッドは基本的に一つのみで、Entityからキーを選びだすセレクタの「GetKeyForItem」メソッド。

使おうとするごとにクラスを一つ作らなければならず、Entityが異なればそれぞれに派生クラスを作らなければならない。今どきこれは大変めんどくさい。

なので、一回汎用的なクラスを作ったら、それを使いまわせるようにしておきたいですね。

戦略としては、「セレクタメソッドをFuncで渡すコンストラクタ」を用意し、先の「GetKeyForItem」メソッドではコンストラクタで渡されたFuncを使う。ことにすれば使いやすくなりそうです。

で、書いてみたのがこれ。

internal class SelectableKeyedCollection<TKey, TItem>
    : KeyedCollection<TKey, TItem>
{
  private Func<TItem, TKey> keySelector;

  internal SelectableKeyedCollection(Func<TItem, TKey> keySelector)
  {
    this.keySelector = keySelector;
  }

  protected override TKey GetKeyForItem(TItem item)
  {
    return keySelector(item);
  }
}

public static class KeyedCollection
{
  public static KeyedCollection<TKey, TItem>
        Create<TKey, TItem>(Func<TItem, TKey> keySelector)
  {
    return new SelectableKeyedCollection<TKey, TItem>(keySelector);
  }
}

結局2つのクラスになっていますが、下のKeyedCollectionクラスは、上のSelectableKeyedCollectionを作るだけのクラスです。そしてSelectableKeyedCollectionクラスはinternal指定としています。

なぜにこんな2段階の作りになっているかというと、

var collection = new SelectableKeyedCollection<X, Y>(x => x.y);

と書かせるより、

var collection = KeyedCollection.Create((X x) => x.y);

のほうがタイプ量も少ないし、Intellisenseもある程度効いてくれます。そして、SelectableKeyedCollectionクラスをintenalとすることで、使う人はその存在を知る必要がなくなり、KeyedCollection<TKey, TItem>を使うために、KeyedCollectionクラスさえ知っていれば良くなります。そのほうが親切だと思うのです。

.net FrameworkのTupleが同じような考え方になっています。

このクラスを使ったサンプルを書いてみます。ここでは、「あす以降の一週間のDateTimeをコレクションに突っ込み、曜日でのアクセスとシーケンシャルなアクセスを行う。」コンソールアプリケーションのサンプルです。なんかもう少し意味のあるサンプルにしたいところですが、それをやると簡潔に書けないので。

「System.Collection.ObjectModel」名前空間と、先のSelectableKeyedCollectionとKeyedCollectionが属する名前空間をusingした上で、

static void Main(string[] args)
{
  var week = KeyedCollection.Create((DateTime dt) => dt.DayOfWeek);

  foreach (var n in Enumerable.Range(1, 7))
  {
    week.Add(DateTime.Today.AddDays(n));
  }

  Console.WriteLine("Today is {0:yyyy/MM/dd (ddd)}", DateTime.Today);
  Console.WriteLine();
  Console.WriteLine("Next 7 days...");

  foreach (var d in week)
  {
    Console.WriteLine(d.ToShortDateString());
  }

  Console.WriteLine();
  Console.WriteLine(
    "Next Friday is {0:yyyy/MM/dd (ddd)}", week[DayOfWeek.Friday]);
}

これをコンパイルして実行すると、以下の結果となります。(ちなみに今日は2014年10月27日)

Today is 2014/10/27 (月)

Next 7 days...
2014/10/28
2014/10/29
2014/10/30
2014/10/31
2014/11/01
2014/11/02
2014/11/03

Next Friday is 2014/10/31 (金)

個人的には、これなら使う気になります。

もし本当に汎用的なクラスに仕上げるなら、KeyedCollection<TKey, TItem>クラスには、コンストラクタがもう2つほどあるので、それらに対する備えをしておけば使い勝手の良いクラスができると思います。

2014年9月11日木曜日

文字列からファイル名に使えない文字を(取り除く|置き換える)

LINQの小ネタをひとつ。

ファイルを作るときのファイル名に、何らかの理由でファイル名として使えない文字が含まれていないかをチェックして、含まれているときには取り除いたり、置き換えたりして処理を続行する。というケースがままあります。

この手の細かい処理が結構見落としがちで、面倒だったりします。

これをLINQを使ってお気楽実装してみます。まぁ、見落としを防ぐことはできませんけどね。

結局のところ、入力された文字列を一文字ずつ見ていって、ファイル名として使えない文字なら取り除いたり、置き換えたりする処理を実装します。「一文字ずつ」見ていくので、LINQの出番ですね。

置き換える(ここではアンダースコア'_')なら、こうですかね。
var invalidChars = Path.GetInvalidFileNameChars();
var converted = string.Concat(
  original.Select(c => invalidChars.Contains(c) ? '_' : c));

で、取り除くならこう。
var invalidChars = Path.GetInvalidFileNameChars();
var removed = string.Concat(original.Where(c => !invalidChars.Contains(c)));


お気楽でしょ。

2013年8月10日土曜日

古くなったログファイルを削除する処理をC#+LINQで。

何かしら業務で利用するシステムであれば、大抵何かあったときの障害解析などに備え、ログファイルが作られることになります。で、そのログファイルは作られっぱなしだとそのうちディスクを圧迫するので、ある一定のルールの下で削除されていくことになるのが一般的だと思います。

その「削除されるルール」は、当然すべてきれいさっぱり消してしまっては、いざという時に役に立たないし、またシステムによってログの残し方の要件が違ったりで、設計や実装も結構厄介なもんです。

ここでは、いくつかログの残し方ルールをあげて、古いログファイルを削除する処理をC#+LINQを使って書いてみようと思います。

ログファイルの構成前提


前提として、ログファイルは以下のような形で残されていることとします。

  • ログフォルダにはログファイルのみが存在する。
  • ログファイルのファイル名は「log_yyyy-MM-dd_nn.log」とする。ただし、yyyyは西暦の4ケタ。MMは月の2ケタ。ddは日の2ケタ。nnはシーケンシャル番号の2ケタ。
  • ログファイルは以下のタイミングで新たに生成される。ただし、同一日のログファイルが存在しない場合はシーケンシャル番号は「0」。存在する場合はシーケンシャル番号の最大+1になるようにする。
    • 日が変わった以降最初のログ出力時。
    • 現在のログファイルが所定のサイズ以上になった時。
    • サービスの起動時。
そしてログファイル削除クラスとしてスタティックな「DeleteOldLogFiles」クラスを作ることとします。

バリエーション1:指定ファイル数のみ残して古いものを削除


指定された数のより新しいファイルを残し、それ以外のものを削除する仕様として処理を考えます。この場合、ファイル名を降順でソートして、新しいほうから所定の数は何もせず、それ以降はファイルを削除する。という処理を考えてみました。

ForEachメソッドを使いたいので、IEnumerableを一旦Listに変換しています。ま、しょうがない。

public static void ByCounts(string path, int keepCount)
{
  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .Skip(keepCount)
    .ToList()
    .ForEach(f => File.Delete(f));
}

ま、シンプルですよね。

バリエーション2:指定ファイルサイズに収まるようにして古いものを削除


バリエーション1の変形ですが、新しいほうからファイルサイズを足して行って、所定のサイズ以下のうちはスキップし、所定のサイズを超えた以降のファイルを削除する処理とするとシンプルに書けますね。

public static void BySize(string path, long size)
{
  long totalSize = 0;

  Directory.GetFiles(path)
    .OrderByDescending(f => f)
    .SkipWhile(f => (totalSize += new FileInfo(f).Length) < size)
    .ToList()
    .ForEach(f => File.Delete(f));
}


バリエーション3:日付が古いものを削除


ファイル名から日付部分を抜き出して、指定日数より以前のものは削除する仕様とします。このメソッドの引数「path」と「days」はまあいいとして、「startPos」はファイル名中の日付文字列の開始位置を、「dateForm」は日付文字列の解析フォームを渡します。

public static void ByDays(string path, int days, int startPos, string dateForm)
{
  var target = DateTime.Today.AddDays(-days);

  Directory.GetFiles(path)
    .Where(f => DateTime.ParseExact(
      Path.GetFileName(f).Substring(startPos, dateForm.Length),
      dateForm,
      System.Globalization.DateTimeFormatInfo.InvariantInfo) < target)
    .ToList()
    .ForEach(f => File.Delete(f));
}

今回のケースでログファイルの保持日数を3日とすると、ログファイル名は「log_yyyy-MM-dd_nn.log」なので、「startPos」は4、「dateForm」は「yyyy-MM-dd」となるので、以下のようにこのメソッドを呼び出すことになります。

DeleteOldLogFiles.ByDays(logFolderPath, 3, 4, "yyyy-MM-dd");


2013年7月27日土曜日

FxCop カスタムルールの作成(配置と実行)

FxCop カスタムルールの作成(ビルドまで)」で作成したルールを実際に動かします。

本当はデバッガで動作を追っかけたいところですが、VS2012Expressではデバッガで外部ツール(プログラム)を実行させるのは難しいんですかね。残念ですがデバッガの使用はあきらめます。

ちなみに、Express以外の有償のエディションであれば、プロジェクトのプロパティで「デバッグ」を開き、「開始動作」を「外部プログラムの開始」に、「外部プログラムの開始」に「FxCopCmd.exe」へのパスを、「コマンドライン引数」に「/f:」に続いて静的分析したいアセンブリのフルパスを、さらに「/c」のスイッチを付けてコンソールに結果を出力させるようにしておくことで、作成したルールをデバッグできる「はず」です。

デバッグ時の注意点としては、VisualStudioもデバッグしたいルールアセンブリを読み込んでしまうので、修正して再配置するときは、VisualStudioを一度終了させる必要があります。面倒ですが仕方ない。

さて、ビルドしたルールアセンブリは、Program FilesのVSのフォルダの、「Team Tools\Static Analysis Tools\FxCop\Rules」のフォルダにコピーします。デバッグする場合は"pdb"ファイルも一緒にコピーします。

作成したルールアセンブリが、正しく動作するかどうかを確認するテスト用アセンブリも作っておきます。VS2012Expressで新規プロジェクトを作成し、クラスライブラリを作ります。名前は「DataTableRuleDummy」とでもします。

テスト用アセンブリには一つクラスを作ります。ルールがすべて引っかかるように、以下のようにしてみました。

public class Dummy
{
  private DataTable table;

  public Dummy(DataTable table)
  {
    this.table = table;
  }

  public int SumOfQuantity
  {
    get
    {
      int sum = 0;
      foreach (DataRow row in table.Rows)
      {
        sum += (int)row["qty"];
      }
      return sum;
    }
  }

  public int FirstOrder
  {
    get { return (int)table.Rows[0]["order"]; }
  }

  public void SetDone()
  {
    foreach (var row in table.Rows.Cast<DataRow>().Where(r => (int)r["f"] == 0))
    {
      row["f"] = 1;
      row["doneAt"] = DateTime.Now;
    }
  }
}

で、テスト用のアセンブリもビルドしておきます。

実行はここではFxCopのコマンド版で試してみます。GUI版やVisualStudioビルド統合版も多分動くはず。

コマンドプロンプトを起動し、以下のように入力します。(もちろん、実際は改行なしです。)

>"C:\Program Files\Microsoft Visual Studio 11.0\Team Tools\
Static Analysis Tools\FxCop\FxCopCmd.exe" /f:D:\Projects\Da
taTableRuleDummy\bin\Debug\DataTableRuleDummy.dll /c

これで実行してみると以下の結果となりました。



4つのルールがすべて出力されています。続いて、テスト用アセンブリを以下のように修正してビルド。その後もう一度FxCopを実行させてみます。

public class Dummy
{
  private DataTable table;

  public Dummy(DataTable table)
  {
    this.table = table;
  }

  public int SumOfQuantity
  {
    get { return table.AsEnumerable().Sum(r => r.Field<int>("qty")); }
  }

  public int FirstOrder
  {
    get { return table.AsEnumerable().First().Field<int>("order"); }
  }

  public void SetDone()
  {
    foreach (var row in table.Select("flag=0"))
    {
      row.SetField("flag", 1);
      row.SetField("doneAt", DateTime.Now);
    }
  }
}


DataTable関連の警告が出なくなりましたね。

と、とてもとても時間がかかりましたが、古式ゆかしきDataSetも、工夫次第でそれなりに使えるということを言いたかったのです。

あと、会社やプロジェクト単位でコーディングスタンダードを用意している場合、コードレビューでスタンダードに適合しているかをチェックするなんてのはやってらんないので、このようにFxCopのカスタムルールを一度作ってしまえば、スタンダードに適合しているか否かのチェックをFxCopに任せられるので、あとがずいぶん楽になります。このために参考になる方もいるのではないかなぁ。と思いました。

2013年7月26日金曜日

FxCop カスタムルールの作成(ビルドまで)

ADO.netのDataSetをLINQableに書くために (本題) ~ FxCopカスタムルールを作成する。」の続きです。今度こそカスタムルールを作ります。ただ、手元にVS2012Expressしかないので、これで作ります。

前エントリで書いたとおり、アセンブリを静的分析し、以下のパターンに適合する場合は警告を出すように作ります。

イマイチ好みじゃないずいぶんマシ
foeach構文でDataTable.Rowsプロパティを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
LINQを使うために、DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。
DataRowCollectionクラスのインデクサを使う。DataTable.AsEnumerable拡張メソッドまたはDataTable.Selectメソッドを使う。(インデクサは使わない。)
DataRowクラスのインデクサを使う。DataRow.Field拡張メソッドDataRow.SetField拡張メソッドを使う。

VS2012Express(今回はDesktopを使いました。)を開き、新規プロジェクトを作ります。「クラスライブラリ」を選択し、プロジェクト名は「DataTableRules」とでもしておきましょう。


作成したプロジェクトに、FxCopのアセンブリを参照するようにします。プロジェクトの「参照設定」を右クリックし、「参照の追加」を選択します。

プロジェクト外の、GACにもない外部アセンブリを参照するので、「参照」ボタンをクリックします。
Program FilesのVSのフォルダの、「Team Tools\Static Analysis Tools\FxCop」にある2つのアセンブリファイル、「FxCopSdk.dll」と「Microsoft.Cci.dll」を選択して追加します。


作成したプロジェクトには、警告メッセージなどを管理するためのXMLリソースが必要になります。
ここでは「CustomRules.xml」というファイルをプロジェクトに追加します。プロジェクトを右クリックして「新規作成」-「追加」-「新しい項目」を選び、XMLファイルをプロジェクトに追加します。


  追加したXMLファイルはリソースとして扱います。ソリューションエクスプローラーで追加したXMLファイルを選び、プロパティで「ビルドアクション」を「埋め込まれたリソース」に変更します。


XMLの中身の編集は後でやります。

さて、前述の警告出力パターンですが、どうすれば検知できるかを考えてみます。

まず、「foeach構文でDataTable.Rowsプロパティを使う。」ですが、これは「DataTable.Rows.GetEnumerable」メソッドが使われたときに警告を出す。と考えればよさそう。

「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」は、そのまんま「Enumerable.Cast<DataRow>」静的メソッドが使われたときに警告を出すでオケ。

「DataRowCollectionクラスのインデクサを使う。」はインデクサの呼び出しですが、CLI的には「DataRowCollection.get_Item」メソッドが使われた時。という扱いになるようです。

「DataRowクラスのインデクサを使う。」も同様に、「DataRow.get_Item」または「DataRow.set_Item」メソッドが使われた時。となるようです。

これら4つのルールすべてにおいて、メソッドの呼び出しを確認することで検知可能ということになります。そこで、まずは基本となる抽象クラスを作ります。

プロジェクト作成時に作られるプレースホルダのクラス「Class1.cs」を使いましょう。まずはファイル名を「BaseDataTableRule.cs」に修正、クラス名もそれに合わせて変更します。内容は「すべてのメソッドを確認する」的な内容になっている…と思われます。なにせドキュメントが少なく、いろいろ試してうまくいっているだけなので、定かじゃないところも残ってます。

using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public abstract class BaseDataTableRule : BaseIntrospectionRule
  {
    protected BaseDataTableRule(string name) : base(
      name, "DataTableRules.CustomRules", typeof(BaseDataTableRule).Assembly) {}

    public override ProblemCollection Check(Member member)
    {
      var m = member as Method;
      if (m != null)
        Visit(m);

      return Problems;
    }
  }
}

抽象クラスを作ったら、1ルールにつき1クラスとなるように、4つのクラスを作り抽象クラスから派生させます。まず「foeach構文でDataTable.Rowsプロパティを使う。」を検出するルール。「NoUseGetEnumeratorMethodToDataTableRows」としました。長いけどこのクラスを誰か人に使ってもらうわけではないので、今回は気にしない。名前がわかりやすいほうが重要。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseGetEnumeratorMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseGetEnumeratorMethodToDataTableRows()
      : base("NoUseGetEnumeratorMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var target = binding.TargetObject;
      var member = binding.BoundMember;

      if (member.Name.Name == "GetEnumerator" && 
          target.Type.FullName == "System.Data.DataRowCollection")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

2つめのクラスは「DataTable.RowsプロパティにEnumerable.Cast<DataRow>拡張メソッドを使う。」これを「NoUseCastMethodToDataTableRows」クラスとして実装します。(18行目と19行目に分けているのは、Web上で表示させるのに横長になりすぎるためで、本来分ける必要はありません。)

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseCastMethodToDataTableRows : BaseDataTableRule
  {
    public NoUseCastMethodToDataTableRows()
      : base("NoUseCastMethodToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName == 
        "System.Linq.Enumerable.Cast<System.Data.DataRow>"
        + "(System.Collections.IEnumerable)")
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして3つめのクラスは「DataRowCollectionクラスのインデクサを使う。」を「NoUseIndexerToDataTableRows」クラスとして実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataTableRows : BaseDataTableRule
  {
    public NoUseIndexerToDataTableRows()
      : base("NoUseIndexerToDataTableRows") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
         "System.Data.DataRowCollection.get_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

そして4つめ。「DataRowクラスのインデクサを使う。」を「NoUseIndexerToDataRow」として実装します。

using System;
using Microsoft.FxCop.Sdk;

namespace DataTableRules
{
  public class NoUseIndexerToDataRow : BaseDataTableRule
  {
    public NoUseIndexerToDataRow() : base("NoUseIndexerToDataRow") {}

    public override void VisitMethodCall(MethodCall call)
    {
      var binding = call.Callee as MemberBinding;
      var member = binding.BoundMember;

      if (member.FullName.StartsWith(
            "System.Data.DataRow.get_Item(", StringComparison.Ordinal) ||
          member.FullName.StartsWith(
            "System.Data.DataRow.set_Item(", StringComparison.Ordinal))
      {
        Problems.Add(new Problem(GetResolution()));
      }

      base.VisitMethodCall(call);
    }
  }
}

あとはXMLリソースの中身の編集です。長いですが、こんな感じ。(適宜改行してます。)
<?xml version="1.0" encoding="utf-8" ?>
<Rules FriendlyName="データテーブルの使い方の規則">
<Rule TypeName="NoUseCastMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0001">
<Name>DataTable.RowsにCast&lt;DataRow&gt;()拡張メソッドを使用しません</Name>
<Description>可読性の向上のため、DataTable.RowsにCast&lt;DataRow&gt;拡張メソッド
を使用しません。</Description>
<Url />
<Resolution>DataTable.RowsにCast&lt;DataRow&gt;拡張メソッドが使用されています。
さらなる条件の絞り込みや、ソートが必要であればDataTable.Select()メソッドを、
そうでなければDataTable.AsEnumerable()拡張メソッドを使用してください。
</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseGetEnumeratorMethodToDataTableRows"
      Category="My.DataTable" CheckId="DT0002">
<Name>foreach構文でDataTable.Rowsを使用した列挙を行いません</Name>
<Description>LINQとの親和性向上のため、DataTable.Rows.GetEnumerable()メソッドを
使用しません。</Description>
<Url />
<Resolution>DataTable.Rows.GetEnumerable()が使用されています。さらなる条件の
絞り込みや、ソートが必要であればDataTable.Select()メソッドを、そうでなければ
DataTable.AsEnumerable()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataTableRows"
      Category="My.DataTable" CheckId="DT0003">
<Name>DataTable.Rowsのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataTable.Rowsのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataTable.Rowsのインデクサが使用されています。テーブル内の各行を列挙
する場合には、DataTable.Select()メソッド、またはDataTable.AsEnumerable()拡張
メソッドを使用してforeach構文で列挙してください。 先頭行のみ取り出す場合には、
DataTable.AsEnumerable()拡張メソッドと、Enumerable.First()を使用して一時変数に
代入後、使用してください。
      例:var firstRow = dt.AsEnumerable().First();</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
<Rule TypeName="NoUseIndexerToDataRow"
      Category="My.DataTable" CheckId="DT0004">
<Name>DataRowのインデクサを使用しません</Name>
<Description>可読性の向上のため、DataRowのインデクサを使用しません。
</Description>
<Url />
<Resolution>DataRowのインデクサが使用されています。テーブル内の各行のカラムに値
を設定、または取得する場合は、DataRow.SetField&lt;T&gt;()拡張メソッド、および
DataRow.Field&lt;T&gt;()拡張メソッドを使用してください。</Resolution>
<Email />
<MessageLevel Certainty="80">Warning</MessageLevel>
<FixCategories>NonBreaking</FixCategories>
<Owner />
</Rule>
</Rules>

ふぅ。これで準備完了。のはず。後はビルド~配置~デバッグ。です。

が、もう限界。眠い。またまた続きにします。しくしく。いい加減終わらせたかったけどなぁ。

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月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年5月23日木曜日

RDBMS利用時のアーキテクチャの選択方針

全エントリのように、各アーキテクチャには一長一短あるわけですが、それでもどれかを選ばないと始まらないので、「僕だったら」こんな方針でアーキテクチャを選ぶ。というところを書きたいと思います。

バッチ系の処理


バッチ系の処理の場合、SELECTで条件に合致したレコードすべてに対して何らかの処理を行う。という形になると思います。この場合は生ADO.net(DbConnection/DbCommand/DbDataReader)が相性が良いと思います。

こいつは古き良きスタイルなので、レコードを一行一行読んで処理して、という形に自然になること。また、DBへの接続と切断をコントロールしやすいのもメリットです。対象のRDBMSに応じて、DbXxx抽象クラスの派生クラスを使うことで、それぞれのRDBMSの特性を生かした実装ができるのもいいですね。

(型あり|型無し)データセットだと、一旦まとめて読みこむ形になるので、バッチ系処理ではかえって制御が難しいように思います。

EF(Entity Framework)もバッチ処理で使いにくいわけではなさそうですが、たとえばSQLServerなんかだとロックエスカレーションによるデッドロックを回避するために、SELECT時にロックヒントを付けたりしますが、こんな感じのRDBMSごとの配慮がいちいち面倒なイメージがあります。実際にバッチ系処理で使ったことはないですが。

Webアプリケーション


Webアプリの場合はプレゼンテーション側のアーキテクチャを何にするかによると思います。僕の場合は好みの問題で、全力でASP.net MVCを推します。この場合はEFを選択するのがたぶん楽です。

どうしてもASP.net (Webフォーム)から逃げられなかった場合は、型無しデータセットかな。

その他 (2層C/S、3層C/S、Webサービス、etc...)


そのほかはケースバイケースになりますが、メリットデメリットを加味しながら、基本的には好みでEFか、型無しデータセットか、生ADO.netのいずれかを選ぶかな。この3つ以外は選択肢から除外。

ただし…


EFを使う場合は、モデルファーストかコードファーストで開発を始めるのがよいと思います。コードファーストはいろいろ批判的な意見も見かけますが、僕は結構好きです。
それと、インデクッスやトリガ、ストアドプロシージャなんかはどう使うか、何処で使うか、どのタイミングで実際に適用するか。ここら辺は十分な配慮が必要でしょう。あと、ビューは使いづらいです。多分。

型無しデータセットを使う場合は、コードのメンテナンシビリティを保つために、以下の2つを守ることがおススメです。
これを守ることで、前のエントリで列挙した型無しデータセットのデメリットがだいぶ減り、
  • DataTableの各レコードに対して、Enumerable拡張メソッドが使いやすくなり、コードが比較的きれいになる。
  • フィールドからのデータ取得時のキャストが不要になる。また、Nullableを使ってDBNullをNULLにマップできる。
と、形無しデータセットでも、少しはイマドキなコードが書けるようになります。例をあげてみましょう。たとえば、取得済みのDataTable「t」から、「Kana」順に「Name」と「Birthday」を取得してイロイロ。しかも「Birthday」はNullable。ならこんなコードになります。

DataTable t = GetTable();

foreach (var row in t.AsEnumerable().OrderBy(r => r.Field<string>("Kana")))
{
  var name = row.Field<string>("Name");
  var birthday = row.Field<DateTime?>("Birthday");
  // name,birthdayを使っていろいろ処理
}
このコードは、当然Selectメソッドで匿名クラスを作ったりしても問題なく行けます。個人的は「これなら使ってもいっか。」という気になります。

そして、生ADO.netを使う場合は、多段usingネストを減らす工夫があるとよいでしょう。

とりあえずそこは、ちょっと長くなったので別エントリで書くことにします。

2013年5月21日火曜日

悩ましいRDBMSの利用時のアーキテクチャの選択

唐突ですが、C#(というか.netのアプリケーション)で、RDBMSを利用するのは、.netの登場から10年以上たった今でもどうするのが正解か?いまだに難しい問題だなぁと思っています。

探してみるとRDBMSと.netの間に入る、よさそうなミドルウェアもなくはないんだけど、チームでの開発だとライセンスのコストや習熟にかかるコストが読みづらく、正直選択がためらわれる。

で、.netの標準の仕組みでデータベースアクセスをしようとすると、今なら以下のいずれかを選択することになりますね(新しい順)。
  • Entity Framework
  • LINQ to SQL
  • 型付きデータセット/形無しデータセット
  • 生ADO.net (DbConnection+DbCommand+DbDataReader)

個人的な感想を言うと、どれも一長一短なのです。
  • Entity Framework
    • 物理層のRDBMSを抽象化した、論理層に対する操作を行う。論理層への操作(LINQを使った処理)が物理層(SELECT等のSQLコマンド)にマッピングされる動作はすばらしいと思う。
    • が、特定のフィールドのみ更新したい場合ないど、UPDATEとの相性がいまいち。
    • それと、物理層への操作は基本的に排する方針のようで、インデックスやらトリガやらビューやらはどう組み込んでいいものやら。要は、RDBMSでお気楽にできる処理が、簡単じゃなかったりすることがしばしば起こる。
    • インデックスとかを取り込みにくいので、Webアプリのように徐々にレコード数が増えていくものであれば、DBのスキーマが定まった後に、必要に応じてインデックスを追加していくのはイケルかと思う。でも、最初からデータ移行で数十万レコードを考慮しないとならんようなケースでは、インデックスやトリガやビュー等を最初から考慮した設計を行いたいんだけど、…なんかすごくやりづらい。
    • そもそも、習熟した人がほとんどいない。
  • LINQ to SQL
    • SQLServer以外が選択肢にならない。
    • 今後のメンテナンス/機能追加が期待できない。
  • 型付きデータセット
    • きらい。
    • …(それ以外の理由が必要?)
    • LINQを意識した作りになっていない。
    • 自動で付加されるクラス名に、すごい名前付けてくれちゃったり。おかげでよほどベテランさんが意識してコードを書かないと読みにくいのなんの。
    • Visual Studioのデザイナ(UI)の出来がイマイチ。よくわからない挙動にイラッとすることも度々。イヤになるほど遅いし。ちょっと複雑なSELECT文だとどうにもエラーになっちゃったり。
    • 特定のフィールドのみを出力するSELECTや、複数テーブルをJOINしたSELECTとかは別のクラスにするの?一つのクラスでSELECTごとに埋まる項目が変わるの?クラスを呼び出す側はメソッドによってデータが入ってたり無かったりするのかな?どうもこう、どう注意して作っても統一性が取れなくて。
  • 形無しデータセット
    • 当然、LINQを意識した作りになっていない。
    • フィールド名によるインデクサを多用せざるを得ないため、文字列リテラルが大発生。文字列リテラルに間違いがあると実行時のエラーとなってしまうのでバグの元が山盛りある状態に。
    • さらにインデクサで取得してもすべて形無し(object)になってしまうので、キャストやらコンバータやら、本来処理したいもの以外のコードでソースが埋め尽くされる。
    • そこに持ってきて、DBNullがオブジェクトのNULLにマップしてくれないので、NULLチェックによる処理もソースを汚し倒してくれる。
    • これはもう誰が書いてもそうなる。
  • 生ADO.net (DbConnection+DbCommand+DbDataReader)
    • イイ言い方をすれば「由緒正しい」。それってつまり「古臭い」。
    • それぞれ構築と廃棄を意識して作る必要がある。usingによるネストが大発生。using使わなかったりするとそれはそれで悲しいコードになりがち。

と、まぁ、悩むことこの上ない。そんなこんなで僕ならどうするか。次のエントリで書いてみたいと思います。(解決にはなりません。あんまり期待しないでほしいっす。)

2013年5月8日水曜日

ファイルが既に存在する場合に、ナンバリングして存在しないファイル名を取得する処理をLINQで。

タイトルにある、ちょっと無理めなお題を考えてみた。

すなわち、Windowsでよくある処理で、たとえば「U:\Test\Test.txt」が存在しない場合はそのままで、存在する場合は「U:\Test\Test (2).txt」を、それも存在する場合は(3)、(4)、…という風にナンバリングし、存在しないファイル名を返す、そんなメソッドを考えてみる。

LINQは意識せず、深く考えずに書いてみたらこうなった。

static string GetNumberedUniqueFilename(string filename)
{
  if (!File.Exists(filename))
    return filename;

  string d = Path.GetDirectoryName(filename),
    fn = Path.GetFileNameWithoutExtension(filename),
    ext = Path.GetExtension(filename);

  for (int i = 2; ; ++i)
  {
    var f = Path.Combine(d, string.Format("{0} ({1}){2}", fn, i, ext));
    if (!File.Exists(f))
      return f;
  }
}


これをLINQ使って書きなおしてみると…。難しいな…。こうか?

static string GetNumberedUniqueFilename(string filename)
{
  if (!File.Exists(filename))
    return filename;

  string d = Path.GetDirectoryName(filename),
    fn = Path.GetFileNameWithoutExtension(filename),
    ext = Path.GetExtension(filename);

  return Enumerable.Range(2, int.MaxValue - 2)
    .Select(i => Path.Combine(d, string.Format("{0} ({1}){2}", fn, i, ext)))
    .SkipWhile(f => File.Exists(f))
    .First();
}


…これはびみょーだなぁ…。

2013年5月3日金曜日

Enumerable.Zipメソッド

2つのコレクションを、同時に列挙したいケースというのはたまにありますよね。たとえば、2つの異なるデータソース上のデータが、何らかのキーでJoinできるわけではなく、単純に格納順序で順番に取り出してなんかしらの処理をして出力したい。とか。

まぁ、こんなケースはたいていデザイン的に問題があったりするんですけど、とはいえリファクタリングするにはなんだか大掛かりになりすぎちゃって、「そこまでしたくないなぁ。」なとき。

こうなると、foreachで2つのコレクションから順次取り出すことはできないので、for文+インデクサを使ったりするわけですが、こんな感じのコードになりますね。

int[] numbers = new[] { 0, 1, 2, 3, 4 };
string[] names = new[] { "Zero", "One", "Two", "Three", "Four" };

for (int i = 0; i < numbers.Length; ++i)
{
  Console.WriteLine("{0}:{1}", numbers[i], names[i]);
}

ところが、今となっては単純な列挙にforとかiとか[]とか、こういうのを見るとなんだか可読性が悪い(…様な)気がして、foreachで置き変えたくなっちゃう。こんなときに使えるのが「Enumerable.Zip」メソッドです。2つのコレクションを、まとめて一つにしちゃってくれる、「.net4」で追加されたメソッドです。たとえば、Tupleと組み合わせることで、こんなコードに置き換えできる。

int[] numbers = new[] { 0, 1, 2, 3, 4 };
string[] names = new[] { "Zero", "One", "Two", "Three", "Four" };

foreach (var x in numbers.Zip(names, Tuple.Create))
{
  Console.WriteLine("{0}:{1}", x.Item1, x.Item2);
}

やっぱりこっちのほうがすっきり見えるわけです。完全にLINQに毒されてますな。自覚あります。

Item1とかItem2とか、このコードくらいスコープが短ければ気にならないけど、もう少し複雑な処理で、名前にも気を使いたいような場合なら、Tupleの代わりに匿名クラスを使うことで、こうも書けますね。

int[] numbers = new[] { 0, 1, 2, 3, 4 };
string[] names = new[] { "Zero", "One", "Two", "Three", "Four" };

foreach (var x in numbers.Zip(names, (number, name) => new { number, name }))
{
  Console.WriteLine("{0}:{1}", x.number, x.name);
}

どちらでも、ケースバイケースで。あるいはお好みで。

2013年4月25日木曜日

Comparison デリゲートを利用した IComparer の実装クラス

訳のわからないタイトル。


事の起こりは、ジェネリックの「SortedDictionary」クラスを使おうと思ったこと。

デフォルトの比較ルールだと、希望する順番にソートされないので、比較関数を置き変えたかった。そのためにはどうするのか?MSDNを引いてみると、



SortedDictionaryのコンストラクタのオーバーロードを確認しても、IComparer<TKey>を指定するものはあっても、Comparison<TKey>を指定できるものはない。えー?このためにクラス一個作んないとダメってことかい?めんどくせ。

List<T>とかのSortメソッドには、Comparison<T>もIComparer<T>もどっちも引き渡せるようになっているのに…。.net 2.0の時代ならいざ知らず…、ちょっと片手落ちじゃないすか?というか、手を入れ損なったのかなぁ。

ま、無いものはしょうがない。とはいえ、クラスを作るにしても似たようなものを何度も作りたくはない。なので、『Comparison<T> デリゲートを利用した IComparer<T> の実装クラス』を作り、汎用的に使いまわせるようにしようと考えた次第。

しかし、ここで悩んだのはそのクラス名。汎用的なIComparer実装クラスなので、「Comparer」クラスにしたかったのだけど、「System.Coolection.Generic.Comparer」クラスが既に存在しているので却下。

悩んだ挙句、「ComparisonComparer<T>」にしました。頭痛が痛いような名前。でもまぁ、名は体を表していることには違いないし、なによりIntellisenceで「Compar...」でリストアップされるので、まぁ、使い勝手は悪くないんじゃないかと思う。

およそ実装はこんな感じ。

class ComparisonComparer<T> : IComparer<T>
{
  private Comparison<T> comparison;

  public ComparisonComparer(Comparison<T> comparison)
  {
    this.comparison = comparison;
  }

  public int Compare(T p1, T p2)
  {
    return comparison(p1, p2);
  }
}

念のため、ジェネリックじゃないIComparerも実装しておいたほうがいいかも。そして、こう使うと。

var dic = new SortedDictionary<string, int>(
    new ComparisonComparer<string>((p1, p2) => string.Compare(p1, p2)));

正直、それほど使い道があるわけじゃない(ざっと見た限り、SortedListやSortedSetではIComparer<T>が必要なので、使い道はありそうだけど、それくらい…)。だけど、いちいちクラス一個作るよりはいいでしょ。