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のちょっとしたプロジェクト説明みたいな部分だけですら破綻しまくってる、単語すら繋げられない、これは酷い。

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

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive