2016年12月18日日曜日

真田丸

ああ、真田丸が終わっちゃった。
面白かったよおおおおお。

来年は一体どうしたらいいんだあ。

2016年10月1日土曜日

今週のモーニング

あらゐけいいちがモーニングでもあらゐけいいちでうれしい。楽しみが増えた。

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されたオブジェクトが特に参照されないのであれば、変数に代入しなくてもちゃんと動くんですね。初めて知った。

もう8月ですね...

ほとんど更新しないまんまもう8月。月日の経つのはほんと早い。

いや、真田丸も面白いし、相変わらず漫画もたくさん読んでるし、ポケモンGOも始めたしで、書くことないわけでもないんですが、やっぱり本筋のC#/LINQ的なネタがないと、他のことも書こうという気になかなかならず...。

ちょっとだけネタができたので、ぼちぼち行きます。

2016年1月20日水曜日

今週のマガジン

「AKB49」が完結しましたね。始まったときはイロモノかと持ったのですが、なかなかどうして面白かったです。

あとはやっぱり「ベイビーステップ」がやっぱり楽しい。今回のような試合じゃないけど、テニスプレイヤーとしての一面を見せてくれるような回があるのがとてもよいです。

2016年1月18日月曜日

「真田丸」が面白い!

おぉ。いつの間にか年が明けておりますね…。ごにょごにょ…。

…さて、今年の大河ドラマ「真田丸」が面白いです!すごく!

大河ドラマはどうしても、最初のほうは、時代の背景や主人公の特に幼少期のエピソードなどの説明が主になってしまい、最初の二か月ぐらいは我慢してみてると、そのうち面白くなってくる。というのがパターンでした。

が、今回はもう最初から面白い。

予想していたのは、長篠の戦いあたりがスタートで、信幸・信繁兄弟の幼少期から入ると思っていました。が、幼少期はぶっとばして、いつも通りの子役からなんてことはせず、武田家滅亡の直前からのスタートで、堺さんと大泉さんに14~5歳をやらせた。たぶん、正解だと思う。

その堺さんと大泉さん。始まる前は「逆じゃね?」と思ったけど、すごくしっくりくる。そんでなにより、おとーちゃんの草刈正雄。もーぴったりすぎ。(昔のNHKの「真田太平記」で幸村(信繁)役だったらしい。)

お話もすばらしい。わかりやすい。展開早くてだれない。そして大河なのにところどころに笑いを差し込んでくる。いーねー!

そして、過去の大河ドラマでよく不満に思っていたのは、戦闘などがあった時にその地理的な位置関係などの説明が少なく、なんでその結果になったのか、よくわからないことが多かった。

しかし、今回は非常にわかりやすい地図と共に背景を説明してくれるので、理解が早くて話に入りやすい。この地図がいい!(オープニングのタイトルロールで、「シブサワ・コウ」の名前を見たときはビビりました。)


ひとまず二話まで終わった時点では先が楽しみで仕方がないです。


…そして、Twitterの「#真田丸どうでしょう」に爆笑。

2015年11月5日木曜日

今週のモーニング

『ピアノの森』が最終回を迎えました。

連載開始から18年とのこと。ものすごい長期連載が大団円を迎えました。
最初の連載はアッパーズで、当時掲載誌で読んでました。アッパーズは好きだったなぁ。
長らく楽しませていただきました。最終回は、ありゃ泣くよ。

お疲れ様でした。次回作はどんなお話でしょうか。楽しみにしてます。

それと、『決してマネしないでください』が次回で最終回とのこと。これも楽しみだっただけに、正直もう少し続けてほしかったなぁ。

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月30日火曜日

『月光~the lunatic~』

イブニングで5週掲載された、ウチヤマユージさんの『月光~the lunatic~』の単行本が発売されたようなので、おススメを。



表題作と他一遍の中編集。イブニング掲載時に読みましたが、すごく良かった。絵も話もとても好み。

静かに始まる第一話。一見他愛ない日常に見えて、ちらほらと異常がちりばめられる。
話が進むと時間軸を行ったり来たりしながら展開していき、少しずつ全容が見えてくる。
すべてが明らかになった後、結末は「んなあほな」と思いつつ、なんだか妙に晴れやかな気分。

この作者さんは知りませんでしたが、今後に期待大です。

2015年6月25日木曜日

『僕だけがいない街』アニメ化決定

『僕だけがいない街』の最新刊の発売はいつかな?と、調べていたらこんなのを見つけました。

『僕だけがいない街』アニメ化決定&最新刊発売!各界の著名人も絶賛

え?アニメ化?まじで?だって、未完結の状態でこれアニメになんかできないでしょ?と思いつつ、記事を読むと、『2016年1月フジテレビ「ノイタミナ」枠でアニメスタート』とあった。

これは全部妄想だけど、アニメが2クールだとすると、2016年6月にアニメが最終回になるわけで。そうすると、2015年7月に単行本6巻が発売で、1冊出るのに半年かかるので、2016年1月に7巻、2016年7月に8巻が、最終巻として出るなら帳尻は合う。一回くらい休載しても何とか間に合うか。

『四月は君の嘘』もそんな感じじゃなかったっけ?あれもノイタミナ。

そーすると、あと12話で完結??


…なんてことを考えながら、はやく6巻読みたいな。単行本で追いかけてるので、続きが気になって。

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年4月16日木曜日

来週のサンデー

銀の匙が載りますか!戻ってきてくれてよかった。
BIRDMENも掲載される号だし。楽しみだー。

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年12月21日日曜日

大河ドラマ『軍師官兵衛』最終回

今年の大河は面白かった。合戦のシーンがもう少し欲しかったかな。とか、鳥取城攻めがまるっとなかったりはちょっと残念でしたが。

来年はどうかな。再来年は今から楽しみ。

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


お気楽でしょ。

せめて「細々と」

ご無沙汰しておりました。。。

えー、最後に(まともに内容のある)エントリを立てたのが去年の8月だったので、すでに1年以上前のことだったりします。ひえー。

なんやかや忙しかったりしたのが、少し落ち着いてきたので、せめて「細々と続いてる」状態を目指して行きたいと思っております。

C#なネタは枯渇気味なので、どのくらい続くかわからんですが。