AnonymousComparer - lambda compare selector for Linq

class MyClass
{
    public int MyProperty { get; set; }
}

static void Main()
{
    // 例として、こんな配列があったとします
    var mc1 = new MyClass { MyProperty = 3 };
    var mc2 = new MyClass { MyProperty = 3 };
    var array = new[] { mc1, mc2 };
    // Distinctは重複を取り除く。でも結果として、これは、2です。
    var result = array.Distinct().Count();
    // 参照の比較なので当然です。では、MyPropertyの値で比較したかったら?
    // DistinctにはIEqualityComparerインスタンスを受け付けるオーバーロードもあります
    // しかしIEqualityComparerはわざわざ実装したクラスを作らないと使えない

    // そこで、キー比較のための匿名Comparerを作りました。
    // ラムダ式を渡すことで、その場だけで使うキー比較のIEqualityComparerが作れます。
    array.Distinct(AnonymousComparer.Create((MyClass mc) => mc.MyProperty));

    // でも、長いし、型推論が効かないから型を書く必要がある
    // Linqに流れているものが匿名型だったりしたら対応できないよ!
    // というわけで、本来のLinqメソッドのオーバーロードとして、記述出来るようにしました
    // ちゃんと全てのIEqualityComparerを実装しているLinq標準演算子に定義してあります
    array.Distinct(mc => mc.MyProperty);

    // 短いし、型推論もちゃんと効くしで素晴らしいー。
    // 匿名型でもいけます(VBの匿名型はC#(全ての値が一致)と違ってKey指定らしいですね)
    var anonymous = new[] 
    {
        new { Foo = "A", Key = 10 },
        new { Foo = "B", Key = 15 }
    };
    // true
    anonymous.Contains(new { Foo = "dummy", Key = 10 }, a => a.Key);
}

と、いう内容のコードをCodePlexで公開しました。LinqのIEqualityComparerって使いにくいよね、を何とかするためのものです。DLLでも何でもなく、ただの100行のコードなのでコピペで使ってくださいな。メソッドはAnonymousComparer.Createしかありません。newを使わせないのは型推論のためです。メソッド経由なら引数の型を書くだけで済み、戻り値の型を書く手間が省けるので……。あとはLinq標準演算子でIEqualityComparerを使うオーバーロードの全てに、キー比較用ラムダ式を受けるオーバーロードが追加されています。使い方、使い道は、まあ、見た通りです。

わざわざzipをダウンロードするのも面倒、という人はCodePlexのソース直接表示でどーぞ。どうせ.txtと.csしか入ってないので。でもダウンロード数とかが増えてると少し嬉しいですね。linq.jsもようやく50超えましたよ、あまりの少なさに笑えない。

以前にも同様のものを書いてた LinqとIEqualityComparerへの疑問 のですが、今回やっと重い腰を上げてまとめてみました。GroupJoinのオーバーロードとか手書きだと死ぬほどダルいですからねえ。と、いっても、やっぱ手書きでやってたら洒落にならないほど面倒くさいので、機械生成でサッと作りました。全然サッとしてないんですけどね。むしろ泥臭い。Linqネタなのでワンライナーで強引に仕上げてみましたよ!

static string data  = @"ここに定義へ移動で出てくるEnumerableのデータを貼り付けてね、と(4000行ぐらい)";

static void Main(string[] args)
{
    var result = data
        .Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
        .Where(s => Regex.IsMatch(s, "public static.+IEqualityComparer"))
        .Select(s => Regex.Replace(s, @"(<.+)(>\(this)", "$1,TCompareKey$2"))
        .Select(s => Regex.Replace(s, @"IEqualityComparer<(.+?)>", "Func<$1,TCompareKey>"))
        .Select(s => Regex.Replace(s, @"comparer", "compareKeySelector"))
        .Select(s => s.Trim(' ', ';'))
        .Select(s => new { Signature = s, Groups = Regex.Match(s, @"^(.+? ){3}(?<method>[^ ]+?)<.+?>\(this (?<args>.+)\)$").Groups })
        .Select(a => new
        {
            a.Signature,
            MethodName = a.Groups["method"].Value,
            Args = a.Groups["args"].Value
                .Split(new[] { ", " }, StringSplitOptions.None)
                .Select(s => s.Split(' ')).Where(ar => ar.Length == 2).Select(ar => ar.Last())
        })
        .Select(a => string.Format("{1} {0} {{ return {2}.{3}({4}{5}); {0}}}{0}{0}",
            Environment.NewLine,
            a.Signature,
            a.Args.First(),
            a.MethodName,
            string.Join(",", a.Args.Skip(1).TakeWhile(s => s != "compareKeySelector").ToArray()),
            (a.Args.Count() == 2 ? "" : ",") + "AnonymousComparer.Create(compareKeySelector)"))
        .Aggregate(new StringBuilder(), (sb, s) => sb.Append(s))
        .ToString();
}

string dataのところにEnumerable.Rangeなんかを右クリックして「定義へ移動」で出てくるメタデータから、のものを全部コピーしてペースト。あとは、それをLinqでゴリゴリ加工すれば出来上がり。です。Select7連打は悪ノリですね。別にRegexの部分は.Replaceを繋げればいいのにね。あと、かなり決めうち成分強めなのと、正規表現が苦手であんまり上手く書けてないところが多かったりとで全く褒められたコードではありません。正規表現は本当に何とかしたいなあ……。

ああ、あと英語が酷い(笑) CodePlexのちょっとしたプロジェクト説明みたいな部分だけですら破綻しまくってる、単語すら繋げられない、これは酷い。

そういえば作ってから気づいたんですが、普通にリフレクションで取得した方が……遙かに……楽!綺麗に……仕上がる! と、気づいてしまったのだけど気づかなかったことにしようそうしよう。

return IEnumerableとyield return

static void Main(string[] args)
{
    var path = @"C:\test.txt";
    var lines = EnumerateAllLines(path).ToArray();
}

static IEnumerable<string> EnumerateAllLines(string filePath)
{
    using (var sr = new StreamReader(filePath))
    {
        return Enumerable.Repeat(sr, int.MaxValue)
            .TakeWhile(s => !s.EndOfStream)
            .Select(s => s.ReadLine());
    }
}

これの実行結果はどうなるでしょうか。答えは、「閉じているTextReaderから読み取ることはできません。」という例外が発生します。当たり前ですか? すみません。Linqばかり触っていると、ついついIEnumerableだから遅延評価だね!と単刀直入に思ってしまっていたりしたのですが、IEnumerableは決して必ずしも遅延評価であるということでは、ない。配列だってIEnumerableなんだよ!という。当然のようなことですが、すっかり頭から抜け落ちていました。反省。

何で例外が発生するかと言えば、EnumerateAllLines(path)の時点でメソッドが呼ばれ、returnで返した時点でusingを抜けてストリームが閉じられてしまう。ので、ToArray()で閉じられたストリームに対して読み込みを始めて、南無。というわけです。ではどうすればいいかというと……

static IEnumerable<string> EnumerateAllLines(string filePath)
{
    using (var streamReader = new StreamReader(filePath))
    {
        var seq = Enumerable.Repeat(streamReader, int.MaxValue)
            .TakeWhile(sr => !sr.EndOfStream)
            .Select(sr => sr.ReadLine());
        foreach (var item in seq) yield return item;
    }
}

yield returnを使ってやれば、コンパイラがイテレータを作るので、遅延実行される。EnumerateAllLines(path)の時点ではメソッド内部は一切通らない。MoveNextが呼ばれて初めてusingを通り、列挙が終わるかDisposeが呼ばれるまではusingを抜けない。という、なって欲しいであろう挙動を取ってくれるわけです。実行ファイルをReflectorで見ると、復元不可能なぐらいグチャグチャなものが出力されていて、あまりの難読化っぷりにビビりますが気にしないことにしませう。

そもそもEnumerable.Repeat(sr, int.MaxValue)のほうを改善してRepeatWithUsing作った方がいい、のではあるのですけど、まあ、それはそれということで。

無限リピート + SQL

上のはただの説明用の例でクソの役にもたたないので、もう少し実用的なものを一つ。

static void Main(string[] args)
{
    var command = new SqlCommand();
    command.CommandText = @"select hogehogehoge";
    var result = command.EnumerateAll(dr => new
    {
        AA = dr.GetString(0),
        BB = dr.GetInt32(1)
    });
}

static IEnumerable<T> EnumerateAll<T>(this IDbCommand command, Func<IDataReader, T> selector)
{
    using (var reader = command.ExecuteReader())
    {
        var seq = Enumerable.Repeat(reader, int.MaxValue)
            .TakeWhile(dr => dr.Read())
            .Select(selector);
        foreach (var item in seq) yield return item;
    }
}

static T[] ReadAll<T>(this IDbCommand command, Func<IDataReader, T> selector)
{
    return command.EnumerateAll(selector).ToArray();
}

シーケンス無限リピートをSQLの読み込みに応用してみるとかどうでしょう。Linq to Sqlのように、とまでは到底行きませんが、匿名型も使えるし、何となくそれっぽい雰囲気は出てるんじゃないかしらん。EnumerateAllの後段にTakeWhileを付けて条件で途中で止めるとか、Take(10)で10件のみ取得とか、それなりに自由に動かせます。

ラムダ式の引数の名前

Linqもそうですが、Linq意外でもFuncやActionのお陰様でラムダ式を渡しまくりな昨今。そうなるとちょろっとした引数の名前付けが面倒くさかったりします。ので、大抵は一文字で済ませるわけです。一文字ってどうなのよ、よくないんじゃないの?というと、ラムダ式のような極端にスコープが短いものは、しっかりした名前がついているもののほうが逆に見づらかったりするので、一文字でいいと思います。面倒くさいからって全て_だの__だので済ませると見づらいので一文字はなるべく死守します。但し引数を利用しない場合は_を使います。ネストしたり、式じゃなくて文になる場合はしっかりした名前を付けます。

a // AnonymousType
i // int
s // string
c // char
d // datetime
g // IGrouping
t // Type
e(ex) // exception
e(elem) // XElement
e(ev) // event
e(ie) // IEnumerable(あんま使わないですが)
ar // array(IEnumerableの時もこれ使ったりする(適当))
ar // IAsyncResult(うわ、被っとる)
fs // fileSystemとかラクダの頭文字を繋げる
x // 適当なのがない場合とか、x => xとか
xs // arrayやらIEnumerableやらどっちでもいー、とヤケクソな時
_ // 引数を式中で利用しない場合

大抵は型の頭文字一つです。AnonymousTypeは、前はtを使ってたんですが、IntelliSenseで出てくるのが'aだし、世間的にもaが主流だしTypeと紛らわしいので、最近はaで書くようにしています。Aggregateなんかは(a,b)=>って昔は書いていたのですが、aが匿名型なので紛らわしいから、(x,y)=>と書くようにしています。徹底はしてません。適当です。

それにしてもeの被りっぷりは酷い。こうなると、不人気頭文字を使いたい。というわけで、ライブラリからTypeの頭文字を取得してみる。アセンブリの取得部分は2009-08-17 - 当面C#と.NETな記録のコードをお借りしました。

// asmNamesは http://d.hatena.ne.jp/siokoshou/20090817#p1 から
// Groupingをバラしているのは、デバッガで中身を見る時にかったるかったから
var list = asmNames
    .SelectMany(s => Assembly.LoadWithPartialName(s).GetExportedTypes())
    .Select(t => t.Name)
    .GroupBy(s => s.First())
    .OrderBy(g => g.Key)
    .Select(g => new { Key = g.Key, Types = g.ToArray() })
    .ToList();
list.ForEach(a => Console.WriteLine("{0} : {1}", a.Key, a.Types.Length));

A : 319
B : 252
C : 773
D : 715
E : 275
F : 263
G : 172
H : 264
I : 887
J : 14
K : 58
L : 249
M : 424
N : 140
O : 189
P : 526
Q : 31
R : 312
S : 1039
T : 644
U : 169
V : 118
W : 272
X : 372
Z : 7
_ : 32

IはInterfaceだから数が多いのはしょうがない。Zは当然としてJ, K, Qは不人気ワードなので狙いどころですね!そしてSは人気あり過ぎ、と。別に考察でも何でもないです、すみません。CamelCaseを短縮結合して、一文字の場合のみを取り上げたら

// SplitCamelWordはasmNamesと同じくsiokoshouさんのコードから
.Select(t => SplitCamelWord(t.Name).Aggregate("", (x, y) => x + y.First(), s => s.ToLower()))
.Where(s => s.Length == 1)

ちゃんと(?)、eが一番人気でした。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive