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

2017年5月17日水曜日

ASP.NET MVCでCQRSを利用してORMをうまく使う

以前(2013年頃)自分が設計した内容のメモです。
今はASP.NETの世界から離れて久しい。

はじめに

CQRS(コマンドクエリ責務分離)という考え方を使って、参照系と更新系でORMとSQLを使い分けていいとこ取りするという趣旨です。

サンプル

サンプルを作りました。
https://github.com/herara-ofnir3/cqrs-sample

CQRS(コマンドクエリ責務分離)とは

CQRSについて詳しくはぜひこちらをどうぞ。

[Greg Young流CQRS - Mark Nijhof]
http://d.hatena.ne.jp/digitalsoul/20100712/1278886009
※ 上記記事のサンプル
https://github.com/MarkNijhof/Fohjin

ORMを使いたい理由と使いたくない理由

更新処理がとても快適

煩雑なSQLの発行処理を書かずとも、エンティティの状態を変化させ、永続化してやればうまいこと更新してくれます。
特に one-to-many や many-to-many の関係をカスケードしてうまく処理してくれるのは、とても素晴らしいメカニズムです。

バッチ処理的な一括更新や大量データを高速に更新するのには向いていませんが、業務アプリケーションの通常の更新処理なら非常にマッチします。

参照系でしぬ

参照系の処理がやりにくいです。
普通なら簡単なSQLを投げてそのまま結果を表示するだけ、みたいな画面ですら、気にすることが多すぎます。

  • 遅延読み込みされるとまずい(N+1問題)から、これとこれをフェッチして...
  • 一回流してみて実際のSQLをチェックしないと...

みたいな調子です。
どうしてもデータの取得が冗長になりがちです。
特にHQLでSQLを再現していると泣けてきます。

よく言われるORMの問題点はこの参照系の問題に集中している気がします。
どう考えてもシンプルにSQL書いたほうがはやくて確実ですし。

ORMの弱点を補うためのCQRS

ORMの得意な更新系の処理だけORM使って、参照系はSQLを書けばいい、という考えに至りました。
そして、参照系と更新系の分割と一貫性のために、CQRSを適用しました。

CQRSはイベントソーシングとセットで語られることが多いのですが、イベントソーシングはスルーします。
データアクセスがクエリ(参照系)とコマンド(更新系)で違うだけの実装です。

その他利点

実際やってみると色々いいところがありました。

良い作りに縛れる
アプリケーションに必要な参照/更新操作がクエリ/コマンドという形でいい感じにコード上に浮き上がってきます。
いわいる「変な作り」というのがやりにくくなります。
テストがしやすい (疎結合になる)

コマンドの発行は全てコマンドバスに投げるだけになるので、
コントローラが依存するのはコマンドバスといくつかのクエリだけとなります。
そのため、アンチパターンとしてありがちなファットコントローラに陥りにくくなります。

依存関係が絞られますし、トランザクションなどの煩雑な処理が隠蔽されるため、テストが非常に簡潔になります。
自然に結合度が下がり、構造がシンプルになります。
おかげでメンバが積極的にテストを書いてくれました。

更新処理のログが簡単にとれる

全てのコマンド(更新操作)はコマンドバスを通して発行されるので、更新処理を簡単に確実にログできます。(コマンドバスにログ処理を挟むだけ)
このあたりの機構にイベントソーシングを組み合わせると強力ですが、ログをとれるだけでも大きな恩恵を得られました。

運用・保守をする上で、現状のデータに至るまでの経緯が全て記録されているというのは、極めて有効でした。

2014年7月11日金曜日

Dapper で例外 "シーケンスに要素が含まれていません" が起きる

Dapper を使っていたらハマったので。(Dapper っていうより LINQ ?)

現象

以下のように Dapper で Single/First メソッドを使うと例外が起きることがあるようです。

var user = connection.Query<User>("select * from Users where UserName = @userName", new { userName = "hoge" }).Single();
System.InvalidOperationException: シーケンスに要素が含まれていません

クエリの結果が必ず1件あるにも関わらず、起きることがあります。また、常になるというわけではありませんでした。(環境にもよる...?)
私が遭遇したケースでは、数十回から数百回に一度程度再現しました。

解決策

原因と理由はよくわかりませんが SingleOrDefault/FirstOrDefault に変更すると発生しなくなります。

var user = connection.Query<User>("select * from Users where UserName = @userName", new { userName = "hoge" }).SingleOrDefault();

参考

2013年5月18日土曜日

C# で Project Euler に挑戦: Problem 6

問題

二乗和の差

最初の10個の自然数について, その二乗の和は,

12 + 22 + ... + 102 = 385

最初の10個の自然数について, その和の二乗は,

(1 + 2 + ... + 10)2 = 3025

これらの数の差は 3025 - 385 = 2640 となる.

同様にして, 最初の100個の自然数について二乗の和と和の二乗の差を求めよ.

ぼくの解答

static void Main(string[] args)
{
    int n = 100;

    var numbers = Enumerable.Range(1, n);

    var sum = numbers.Sum();
    var sumSquare = sum * sum; // 和の二乗を求めます。
    var squreSum = numbers.Sum(i => i * i); // 二乗数の和を求めます。

    var answer = sumSquare - squreSum;

    Console.WriteLine(answer);
    Console.ReadLine();
}

解説

あまり説明するところがありません... ごく普通に 1 から 100 までの総和の二乗と、二乗の総和を求めています。

この問題はなんか簡単ですね。もっと面白い解き方があるのかな。

2013年5月16日木曜日

C# で Project Euler に挑戦: Problem 5

問題

最小の倍数

2520 は 1 から 10 の数字の全ての整数で割り切れる数字であり, そのような数字の中では最小の値である.

では, 1 から 20 までの整数全てで割り切れる数字の中で最小の正の数はいくらになるか.

ぼくの解答

static void Main(string[] args)
{
    int n = 20;

    int multiple = 1;
    int counter = 1;

    var numbers = Enumerable.Range(1, n);

    foreach (var i in numbers)
    {
        while (counter % i != 0)
        {
            counter += multiple;
        } 

        multiple = counter;
    }

    Console.WriteLine(multiple);
    Console.ReadLine();
}

解説

最小公倍数を求める問題です。単純に 1 から 20 まで順番に最小公倍数を見つけていきます。

最小公倍数を multiple として初期値を 1 とします。カウンタ用の変数(counter)も初期値を 1 にします。

  1. この時点の最小公倍数(1) と 1 の最小公倍数を求める。
    • 1 / 1 = 1 ... 0 => 割り切れるので最小公倍数1 に更新。
  2. この時点の最小公倍数(1) と 2 の最小公倍数を求める。
    • 1 / 2 = 0 ... 1 => 割り切れないので countermultiple を加算。
    • 2 / 2 = 1 ... 0 => 割り切れるので最小公倍数2 に更新。
  3. この時点の最小公倍数(2) と 3 の最小公倍数を求める。
    • 2 / 3 = 0 ... 2 => 割り切れないので countermultiple を加算。
    • 4 / 3 = 1 ... 1 => 割り切れないので countermultiple を加算。
    • 6 / 3 = 2 ... 0 => 割り切れるので最小公倍数6 に更新。
  4. この時点の最小公倍数(6) と 4 の最小公倍数を求める。
    • 6 / 4 = 1 ... 2 => 割り切れないので countermultiple を加算。
    • 12 / 4 = 3 ... 0 => 割り切れるので最小公倍数12 に更新。
  5. この時点の最小公倍数(12) と 5 の最小公倍数を求める。
    • 12 / 5 = 2 ... 2 => 割り切れないので countermultiple を加算。
    • 24 / 5 = 4 ... 4 => 割り切れないので countermultiple を加算。
    • 36 / 5 = 7 ... 1 => 割り切れないので countermultiple を加算。
    • 48 / 5 = 9 ... 3 => 割り切れないので countermultiple を加算。
    • 60 / 5 = 12 ... 0 => 割り切れるので最小公倍数60 に更新。
  6. この時点の最小公倍数(60) と 6 の最小公倍数を求める。
    • 60 / 6 = 10 ... 0 => 割り切れるので最小公倍数60 に更新。
  7. ... という風に続く。

他のプローチ

調べたら他にも良さそうなやり方がありました。最大公約数を元に解くのとか、数学っぽくていいですね。ぼくの頭では思いつきませんでした...

2013年5月13日月曜日

C# で Project Euler に挑戦: Problem 4

問題

最大の回文積

左右どちらから読んでも同じ値になる数を回文数という. 2桁の数の積で表される回文数のうち, 最大のものは 9009 = 91 × 99 である.

では, 3桁の数の積で表される回文数のうち最大のものを求めよ.

ぼくの解答

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        var threeDigitNumbers = Enumerable.Range(100, 900).ToList();
        var answer = threeDigitNumbers
            .SelectMany(i => threeDigitNumbers.Select(j => i * j))
            .Where(i => i.IsPalindrome())
            .Max();

        Console.WriteLine(answer);
        Console.ReadLine();
    }
}

public static class NumberHelper
{
    public static bool IsPalindrome(this int n)
    {
        return n.Digits().SequenceEqual(n.Digits().Reverse());
    }

    public static IEnumerable<int> Digits(this int n)
    {
        if (n == 0)
            yield return 0;

        const int radix = 10;
        var q = n;

        while (q != 0)
        {
            yield return q % radix;
            q /= radix;
        }
    } 
}

解説

この問題のポイントは、どうやって回文数かどうか判定するか、でしょうか。以下のどちらかになると思います。

  1. 文字列で判定する
  2. 各桁の数値をリストにして判定する

文字列でやるほうが簡単ですが、せっかくなので数値の桁を列挙する Digits() メソッドを作って解いてみました。

ある数値の桁の求め方

求めたい進法の基数で割っていって余りを取り出すと桁を求めることができます。例えば 4603 の場合、手順は以下です。

  1. 4603 / 10 = 460 ... 3 (商を次の割られる数へ回します)
  2. 460 / 10 =  46 ... 0
  3.   46 / 10 =   4 ... 6
  4.    4 / 10 =   0 ... 4 (商が 0 になったら終了です)
回文数かどうか

上記のように桁のリストを求めるか、文字列に変換して内容を反転させて元のものと同じかどうか調べればよいですね。

今回は int の拡張メソッドに IsPalindrome というメソッドを用意しています。内容は簡単です。

上記の Digits メソッドで桁を列挙し、Reverse メソッドで反転させたものと同じかを、SequenceEqual メソッドで調べます。SequenceEqual メソッドが true を返せば回文数です。

三桁の数字の積

単純にループしても良いですが、せっかく C# なので LINQ です。

はじめに三桁の数字のリストを作ります。Enumerable.Range(100, 900) で簡単ですね。(100 ∼ 999)

組み合わせを作ってそれらをかければいいので、SelectMany と Select メソッドを使って簡単に記述できます。

実行効率

この解は実行効率が悪いです。三桁の数字の組み合わせを全部作ってしまう点がネックですね。
実行効率でいえば、999 * 999 から 999 * 998, 999 * 997 ... とデクリメントしながら積を求め、回文数になった時点で処理を中断するほうが良いです。

2013年5月9日木曜日

C# で Project Euler に挑戦: Problem 3

問題

最大の素因数

13195 の素因数は 5, 7, 13, 29 である.

600851475143 の素因数のうち最大のものを求めよ.

ぼくの解答

static void Main(string[] args)
{
    var n = 600851475143;

    // まず約数を求め、その中から素数を見つけることで素因数を獲得します。
    var answer = n.Divisors().Where(i => i.IsPrime()).Max();
    Console.WriteLine(answer);

    Console.ReadLine();
}
public static class NumberHelper
{
    public static IEnumerable<long> Divisors(this long n)
    {
        long minor = 1;
        long greater = n;
        var counter = minor;

        while (counter < greater)
        {
            if (n % counter == 0)
            {
                minor = counter;
                greater = n / minor;

                yield return minor;
                yield return greater;
            }

            counter++;
        }            
    } 

    public static bool IsPrime(this long n)
    {
        if (n < 2) return false;
        if (n == 2) return true;
        if (n%2 == 0) return false;

        for (var i = 3; i*i < n; i += 2)
        {
            if (n%i == 0) return false;
        }

        return true;
    }
}

解説

素因数は約数を求めた後、その中から素数を見つけるのが簡単そうだったので、約数を求めるメソッドと、簡単な素数判定メソッドを拡張メソッドで用意しました。

素因数のリストが手に入れば、あとは最大値を返すだけです。

約数の求め方

約数を求めるときは 1 から順に試しに割っていきますが、もし割り切れたら、割った数(カウンタ)除算の結果の両方が約数なので、同時に約数として返していきます。

このとき、除算の結果(商)より大きい約数が今後登場することはないので、終了値を商に置き換えて行きます。

例えば 20 の約数を求めるときは、

  1. 1 で割ってみる -> 20 / 1 = 20 -> 120 が約数 -> カウンタの終了値を 20
  2. 2 で割ってみる -> 20 / 2 = 10 -> 210 が約数 -> カウンタの終了値を 10
  3. 3 で割ってみる -> 割り切れないので次
  4. 4 で割ってみる -> 20 / 4 = 5 -> 45 が約数 -> カウンタの終了値を 5
  5. カウンタが終了値(5)になったので終わり。約数は、1, 20, 2, 10, 4, 5

という感じです。

素数判定

簡単な試し割りです。一度約数を見つけて、その後素数判定をしているので簡単な試し割りで十分かなということで。

2013年4月29日月曜日

C# で Project Euler に挑戦: Problem 2

問題

偶数のフィボナッチ数

フィボナッチ数列の項は前の2つの項の和である. 最初の2項を 1, 2 とすれば, 最初の10項は以下の通りである.

1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

数列の項の値が400万を超えない範囲で, 偶数値の項の総和を求めよ.

ぼくの解答

static void Main(string[] args)
{
    int n = 4000000;
    var answer = FibonacciNumbers(n - 1)
        .Where(i => i%2 == 0)
        .Sum();

    Console.WriteLine(answer);
    Console.ReadLine();
}

static IEnumerable<int> FibonacciNumbers(int max)
{
    foreach (var i in FibonacciNumbers())
    {
        if (max < i)
            yield break;

        yield return i;
    }
} 

static IEnumerable<int> FibonacciNumbers()
{
    var previous = 0;
    var current = 1;
    var next = 0;

    while (true)
    {
        next = previous + current;
        yield return next;

        previous = current;
        current = next;
    }
}

解説

問題を解くだけならもっとシンプルになるんですが、LINQっぽく(?)処理しようと思いました。

フィボナッチ数の無限シーケンスを返すメソッドを作って、最大値でブレークするオーバロードを用意。これで Where, Sum メソッドで簡単に処理できます。

2013年4月28日日曜日

C# で Project Euler に挑戦: Problem 1

問題

3と5の倍数

10未満の自然数のうち, 3 もしくは 5 の倍数になっているものは 3, 5, 6, 9 の4つがあり, これらの合計は 23 になる.

同じようにして, 1000 未満の 3 か 5 の倍数になっている数字の合計を求めよ.

ぼくの解答

static void Main(string[] args)
{
    int n = 1000;

    var answer = Enumerable
        .Range(1, n - 1)
        .Where(i => i % 3 == 0 || i % 5 == 0)
        .Sum();

    Console.WriteLine(answer);
    Console.ReadLine();
}

解説

LINQって便利ですねぇ...

C# で Project Euler に挑戦: はじめに

Project Euler ってなに?

数学の問題をプログラミングで解いていくチャレンジプロジェクトです。
今のところ(2013/04/28時点)400問を超える問題があり、様々なプログラミング言語で世界中の人が挑戦しています。

公式サイトでアカウントを作って、たくさん問題を解いたり、問題を解いた後に参加できるレビューなどに参加すると、Award (Xboxとかの実績みたいなもの)がもらえます。

C# とアルゴリズムの勉強に

数年前から会社の新入社員教育のお題として、比較的簡単な問題を利用させてもらっていたのですが、自身の勉強のためにやってみることにしました。

日頃業務アプリケーション開発ばかりで込み入ったアルゴリズムなどをあまり考えることがなく、新鮮で面白いというのと、ちょっと勉強しておきたいなぁ、というのが動機です。

せっかくなのでC#らしい解き方を心がけたいと思います。例えば、LINQとか。

現時点の成果物

新入社員に教える手前、自分でもある程度解いておこうと思って作ったものが既にあるのですが、もうだいぶ前のことなので、改めてやって行きたいと思います。

のんびり

数学やアルゴリズムなどの分野は非常に苦手なのですが、興味があるのとプログラマとして重要な素養だと思うので、楽しんでやって行きたいと思います。

解いた問題

2012年12月15日土曜日

[C#]式木とLINQ

式木がどんな風に役に立っているか、みんな大好き LINQ で見ていきたいと思います。
普段 LINQ はモリモリ使っていますが、その中身はボンヤリとしか理解していなかったのでおさらいしてみました。

LINQと式木の概要

例として LINQ to SQL を挙げてみていきます。LINQ to SQL で以下のようなクエリ式を書いた場合、

IQueryable<Student> query =
    from
        student in db.Student
    where
        student.Gender == "Male" 
    select
        student
    ;

db.Student を foreach して Genderプロパティ が "Male" の項目を探す... ではなく。
ご存知の通り実際にはこんな感じのSQL文が生成され、ADO.NET によって実行されます。

SELECT 
    [t0].[Id], 
    [t0].[Name], 
    [t0].[Gender]
FROM 
    [dbo].[Students] AS [t0]
WHERE 
    [t0].[Gender] = @p0
-- @p0 = 'Male'

このSQL文を作る過程に式木が利用されています。まず、上記のクエリ式は実際にはシンタックスシュガーで、以下の様なメソッド式に変換されます。

db.Student.Where(student => student.Gender == "Male");

さらにWhereメソッドは拡張メソッドなので、こうなりますね。

Where(db.Student, student => student.Gender == "Male");

この式がこんな感じの式木として扱われます。

同等の Expression を組み立てるとこんな感じです。

var whereMethod = typeof (Queryable)
    .GetMethods()
    .First(m => m.Name == "Where")
    .MakeGenericMethod(new Type[] { typeof(Student) });

var paramStudent = Expression.Parameter(typeof (Student), "student");

var expression =
    Expression.Call(whereMethod,
        Expression.Constant(db.Student),
        Expression.Lambda<Func<Student, bool>>(
            Expression.Equal(
                Expression.Property(paramStudent, "Gender"),
                Expression.Constant("Male")
                ),
            paramStudent
            )
        )
        ;

LINQ to SQL では結果を取得する際、まず式木を評価・解析することで適切なSQL文を作成してくれています。

LINQ to XXXXX

式自体(式木)を評価することで、LINQ to SQL や LINQ to Entities では式を SQL に変換しますが、LINQ to XML, LINQ to DataSet, LINQ to NHibernate ... などでは式を各種データソースに適切なアクセス方法に変換しています。

このような仕組みのおかげで統一的な式で、各種データソースへのアクセス、操作をうまく統合しています。
これが LINQ (統合言語クエリ) と呼ばれるゆえんであり、式木を最も活用している例でもあります。

応用してみる(?)

例えば LINQ to SQL では一括の UPDATE や DELETE ができません。例えば先程の例で 性別(Gender) が 男(Male) の生徒を全て削除しようと思うと以下のようになります。

var query =
    from
        student in db.Student
    where
        student.Gender == "Male"
    select
        student
    ;

db.Student.DeleteAllOnSubmit(query);
db.SubmitChanges();

この操作は以下の様な SQL を発行します。まず一度 Gender = 'Male' な生徒を全て取得してから...

SELECT 
    [t0].[Id], 
    [t0].[Name], 
    [t0].[Gender]
FROM 
    [dbo].[Students] AS [t0]
WHERE 
    [t0].[Gender] = @p0
-- @p0 = 'Male'

それぞれ1つずつ DELETE句 を発行していきます。

DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 1
DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 2
DELETE FROM [dbo].[Students] WHERE [Id] = @p0 -- @p0 = 3
...

これはあまりスマートとはいえないやり方ですね。本来以下のような SQL を発行すべきです。

DELETE FROM [dbo].[Students] WHERE [Gender] = @p0 -- @p0 = 'Male'

このように LINQ to SQL は更新や削除の操作がちょっと弱いです。しかしこの問題に対しては、自前の拡張メソッドを作って上記のような適切な SQL を発行するように実装することで対処できます。
以下の記事が大変参考になりますので、是非ご覧になってみて下さい。

もうちょっと具体的には

今回ご紹介した LINQ の仕組みは、IQueryableインターフェイス 及び IQueryProviderインターフェイス の組み合わせで実現されています。
以下の記事が大変わかり易く参考になりますので、紹介させて頂きます。

IQueryable と IQueryProvider の仕組みが把握できると、独自の LINQ to XXXXX を作ることができて夢が広がりますね。

さいごに

LINQ初心者の方や、式木がよくわからないぜ... という方の参考になれば幸いです。

※ 余談ですが http://ufcpp.net/ さんの C# の情報の充実度はハンパじゃないですね... C# について何か調べると必ずといっていいほど検索で挙がってきます。MSDNよりわかりやすい事が多く、いつも参考にさせて頂いています。

2012年12月4日火曜日

[C#]dynamicと拡張メソッド

ひょっとして dynamic って拡張メソッドもイケるのかな?とふと思ったので試してみました。

こんな感じの適当なクラスと拡張メソッドを用意します。

class Hoge
{
    public string Method()
    {
        return "Hoge's instance method.";
    }
}

static class HogeHelper
{
    public static string ExtensionMethod(this Hoge hoge)
    {
        return "Hoge's extension method.";
    }
}

まずは普通にインスタンスメソッドを dynamic から呼び出します。

var hoge = new Hoge();
dynamic dynamicHoge = hoge;

System.Console.WriteLine(dynamicHoge.Method());
 

結果はこんな感じで普通です。

Hoge's instance method.
 

では本題のこうしたときです。

System.Console.WriteLine(dynamicHoge.ExtensionMethod());

拡張メソッドは単なるシンタックスシュガーなので、

hoge.ExtensionMethod();

このような拡張メソッドの利用は、

HogeHelper.ExtensionMethod(hoge);

こんな風に展開されて実行されます。
なので、Hoge に ExtensionMethod というメソッドがあるわけではなく、dynamic はバインドできないはずです。

さっきのコードを実行してみます。

System.Console.WriteLine(dynamicHoge.ExtensionMethod());
Runtime Binder Exception: 'Hoge' に 'ExtensionMethod' の定義がありません

はい、予想通りでした。もしかしたら拡張メソッドもバインドできてしまうのか...!? と思ったのですが、そんなことはありませんでした。

2012年11月28日水曜日

[C#]式木入門

C#の式木について勉強しながら、勉強したことの理解を書いていきます。

式木ってなに?

式木(Expression tree)とは、式(数式)を木構造で表したものの事です。
以下の様な式を例にします。

int result = 5 + 7 * 3; // result == 26
 

この式をみるとき、加算演算子(+)をAdd関数、乗算演算子(*)をMultiply関数とするとこんな感じですね。

int result = Add(5, Multiply(7, 3));
 

ちょっと改行とかインデントを入れてみます。

int result = 
    Add(
        5, 
        Multiply(
            7, 
            3
            )
        );
 

なんとなく木構造にみえてきませんか?図にしてみます。

はい、どーみても木です。このように式は木で表現することができます。これが式木です。

なににつかうの?

式が木で表せることがわかりました。しかしこれは何の役に立つのでしょうか。

式を組み立てることができる

木構造を作ってあげる事で、好きな式を組み立てることができるようになります。

式を分析することができる

式を木構造にしてやることで、式を分析することができます。

つまり...どういうことだってばよ?

プログラム(式)を作ることができます。またプログラム(式)を分析することができます。

C#での式木

C#(.NET Framework)では式木を扱うための仕組みが用意されています。
System.Linq.Expressions 名前空間に式木を扱うためのオブジェクトが揃っています。

C#の中で式木をオブジェクトとして扱えるということは、C#の中でC#を書くというようなことができるということです。
つまり、プログラムの中でプログラムを作ったり、分析したりできます。いわいるメタプログラミングが可能になります。

さっきの式を組み立ててみる

さっきの簡単な式をC#で組み立てる例を示します。

// 式: 5 + 7 * 3
Expression body = 
    Expression.Add(
        Expression.Constant(5), 
        Expression.Multiply(
            Expression.Constant(7), 
            Expression.Constant(3)
            )
        );
 

このように、式は Expression型 として扱います。Expression型にはいろいろな式を表すための派生型が用意されています。
Expression型にファクトリメソッド(上の例のAddメソッドやConstantメソッドなど)が用意されているので、そこから各派生型のExpressionを作成できます。

加算演算子はAddメソッド、乗算演算子はMultiplyメソッドで作れます。5, 7, 3 などの定数はConstantメソッドで作れます。

このようにして作った式を実行したい場合は、以下のようにします。

Expression<Func<int>> lambda = Expression.Lambda<Func<int>>(body) // () => 5 + 7 * 3;
Func<int> func = lambda.Compile();
int result = func(); // result == 26
 

まず、Expression.Lambdaメソッドを使って、作った式をラムダ式に変換します。この場合は「() => 5 + 7 * 3」というラムダ式ですね。

次に作ったラムダ式を実行可能なデリゲートへコンパイルします。
コンパイルした後は通常のデリゲートと同じなので、普通に実行できます。

まとめ

式木を使うことでC#でメタプログラミングができます。プログラムのなかでプログラムを作るという面白いことが可能です。

こんどは

次は実際に式木がどんな事に役に立つのか、みてみたいと思います。

参考にさせていただきました

2012年11月27日火曜日

[C#]Equalsメソッドの実装について

Equalsメソッドと等値演算子(==)の実装、つまり等価比較について自分の理解が怪しかったので改めてまとめてみました。

きっかけは @xin9le さんのこちらのツイートです。

私は謂わいる ValueObject の実装で、よく値比較のクラスを作るために Equals(とGetHashCode) をもりもり実装していたので、「なんか自分間違ってるのかも!?」と心配になりました。

はじめに

まずはじめにEqualsメソッドや等値演算子(==)の実装について、仕組みや注意点、実装ポリシーをおさらいです。以下の記事が大変参考になります。

Equalsメソッドと等値演算子の実装には、いくつかのケースが考えられるので、それぞれ見ていきたいと思います。

値型の等価比較について

値型のEqualsメソッドをオーバーライドしない場合、既定の実装(ValueTypeの実装)により、定義された各フィールドの等価性を調べるように実装されています。

ただし、この比較はリフレクションを用いて行われるので、パフォーマンスが求められる場合は自分でEqualsメソッドをオーバーライドして実装する必要があります。
(参考:値型については equals と等値演算子をオーバーライドします)

また、値型でEqualsメソッドを実装する場合は、等値演算子を実装する必要があります。この場合、Equalsメソッドと等値演算子は同じ結果を返すようにします。

このように値型の等価比較はあまり複雑なことはありません。通常、値型を作成する場合は、Equalsメソッドと等値演算子を合わせて実装するのが良いようです。

話がややこしくなってくるのは、参照型の場合です。

参照型の等価比較について

参照型のEqualsメソッドは、既定の実装では参照の等価性を評価します。
また、等値演算子も同様に参照の等価性を評価します。(参照型なので当然ですね)

しかし、参照型でも値の比較によって等価性を評価する場合があります。
例えば、文字列型(string)は参照型ですが、Equalsメソッドも等値演算子も値の比較で等価性を評価するように実装されています。

これは参照型であっても、文字列型(string)は値のような特性を持つためにこのようになっています。(たぶん)
文字列型(string)が参照比較だった場合、直感的な挙動でなくなると思うので、なんとなくやりづらそうです。

値型と参照型の選択

「じゃあややこしいから値っぽい型は全部値型でいいじゃん!」と思うわけですが、そういうわけにもいかない事情があります。

値型は代入時にコピーされるので、サイズが大きいと高コストになってしまいます。(逆にサイズが十分小さいなら値型のほうが高速です)
また、ボックス化やボックス化解除が頻繁に発生する場合も高コストになります。

そうなると、値のような特性を持つ型でも、サイズが大きい、ボックス化がよく起きる、というようなケースでは参照型のほうが適切になります。(stringはサイズが大きいですね)

まとめ

というわけで、値のような特性を持つ型については、参照型でもEqualsや等値演算子を実装するケースはあります。
ただ「じゃあそれってどのくらいあるの?」となると、私の想像できる範囲だと最初に挙げた ValueObject くらいしか思いつきません。

なので、そういったオブジェクトを作る必要がなければ、実装することはあまりなさそうです。
あと、Equalsの実装にはGetHashCodeを合わせて実装する必要がありますし、その際にも気をつける必要のある実装ポリシーがいろいろあって、なかなか大変です。

なんとなく自分の理解がそれほど外れた所になさそうな気がして一安心ですが、あまり深く考えずにバシバシ参照型で ValueObject を作っていたので、値型で作ったほうが適切でないか見直しが必要そうです。

あと、Equalsを実装する場合は、タイプセーフに比較できる IEquatable<T> インターフェイスを積極的に使ったほうがよさそうです。

2012年10月20日土曜日

[C#]IDataReader をちょっと使いやすくする

IDataReader を使うときそのままだとこんな感じだと思うんですが、

int intValue = (int) reader["int_field"];
string strValue = (string) reader["str_field"];
 

NULL のときとかちょっと面倒です。(なんで IDataReader.IsDBnull には IsDBNull(string name) みたいなオーバーロードがないんだろう

Nullable<int> nullableValue = null;
if (!reader.IsDBNull(reader.GetOrdinal("nullable_field")))
{
    nullableValue = (int) reader["nullable_field"];
}
 

なのでこんな風にできるようにしたい。

int intValue = reader.Field<int>("int_field");
string strValue = reader.Field<string>("str_field");
Nullable<int> nullableValue = reader.Field<Nullable<int>>("nullable_field");
int ifNullValue = reader.Field<int>("if_null_field", ifNull:-1);
 

3.5以上なら拡張メソッド作ればいいんですが... 残念ながら 2.0 な環境だったのでこんなヘルパクラスを作って実装しました。

まあ実際は VB.NET なんですが... これで IDataReader が少し使いやすくなりました。

2012年10月3日水曜日

[C#]式木を触ってみる

メタプログラミングには以前から興味があったので、ぜひ式木について知りたい!というわけで。
こんなケースで考えてみました。

Equals と GetHashCode の実装

Equals と GetHashCode を実装するとき、大抵はプロパティや内部フィールドの比較を繋げるだけのことが多いので、その式を作ってくれる EqualityComparer があると嬉しいなあ。
※ まあ ReSharper があれば自動で実装してくれますが…

たぶんこんなクラスがあったら、


class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }
}

こんな感じに実装すると思います。

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Version) obj);
    }

    private bool Equals(Version other)
    {
        return Major == other.Major && Minor == other.Minor;
    }

    public override int GetHashCode()
    {
        return Major.GetHashCode() ^ Minor.GetHashCode();
    }
}

これの実装を EqualityComparer に移したらこんな感じになるでしょうか。※ null判定は省略します

class VersionEqualityComparer : IEqualityComparer<Version>
{
    public bool Equals(Version x, Version y)
    {
        return x.Major == y.Major && x.Minor == y.Minor;
    }

    public int GetHashCode(Version obj)
    {
        return obj.Major.GetHashCode() ^ obj.Minor.GetHashCode();
    }
}

この EqualityComparer の Equals と GetHashCode の式を作ってくれる汎用EqualityComparer を作りたいと思います。

使い方をきめる

どんな風に使うかですが…

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    private static readonly ComplexEqualityComparer<Version> Comparer = new ComplexEqualityComparer<Version>()
        .AddMember(v => v.Major)
        .AddMember(v => v.Minor)
        .Compile();

    public override bool Equals(object obj)
    {
        return Comparer.Equals(this, obj as Version);
    }

    public override int GetHashCode()
    {
        return Comparer.GetHashCode(this);
    }
}

こんな感じで実装に利用するプロパティやフィールドをラムダ式で指定できるようにします。

Compile メソッドを呼ぶと指定されたプロパティやフィールドを用いた式がコンパイルされて、あとは使うだけになる、という感じです。

書きやすくするために安直な感じですが、thisを返してメソッドチェーンできるようにしておきます。

メンバの指定を実装する

Equals に利用する “x.PropertyName == y.PropertyName” という式と、GetHashCode に利用する “obj.GetHashCode()” という式を作ります。

それぞれあとで “&&” や “^” でつなぐので、リストに放り込んでいきます。

    private readonly ParameterExpression _paramX = Expression.Parameter(typeof(T), "x"); // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY = Expression.Parameter(typeof(T), "y"); // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj = Expression.Parameter(typeof(T), "obj"); // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList = new List<BinaryExpression>();
    private readonly IList<Expression> _getHashCodeList = new List<Expression>();

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

式を完成させる

Complile メソッドで式を完成させて、コンパイルをかけます。

メンバの指定の際にリストに放り込んでおいた式を “&&” や “^” で繋いで組み立てます。
最後に Expression.Lamda<>() でコンパイルして出来上がったデリゲートを保存します。

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

できあがり

だいたいこんな感じになりました。ほんとは null のチェックとかも必要ですが面倒なのでとりあえず。

public class ComplexEqualityComparer<T> : IEqualityComparer<T>
{
    public ComplexEqualityComparer()
    {
        _compiled = false;
        _paramX = Expression.Parameter(typeof(T), "x");
        _paramY = Expression.Parameter(typeof(T), "y");
        _paramObj = Expression.Parameter(typeof(T), "obj");
        _equalsList = new List<BinaryExpression>();
        _getHashCodeList = new List<Expression>();
    }

    private bool _compiled;

    private readonly ParameterExpression _paramX; // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY; // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj; // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList;
    private readonly IList<Expression> _getHashCodeList;

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

    private void CompileIfNotCompiled()
    {
        if (_compiled) return;
        Compile();
    }

    public bool Equals(T x, T y)
    {
        CompileIfNotCompiled();
        return _compiledEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        CompileIfNotCompiled();
        return _compiledGetHashCode(obj);
    }
}

テスト

    [Test]
    public void UseCase()
    {
        var eqComparer = new ComplexEqualityComparer<Version>()
            .AddMember(v => v.Major)
            .AddMember(v => v.Minor)
            .Compile();

        var version_1_0 = new Version(1, 0);
        var version_2_3 = new Version(2, 3);

        Assert.IsTrue(eqComparer.Equals(version_1_0, version_1_0));
        Assert.IsTrue(eqComparer.Equals(version_2_3, version_2_3));
        Assert.IsTrue(eqComparer.Equals(version_2_3, new Version(2, 3)));
        Assert.IsTrue(eqComparer.Equals(new Version(2, 3), version_2_3));

        Assert.IsFalse(eqComparer.Equals(version_1_0, version_2_3));
        Assert.IsFalse(eqComparer.Equals(version_2_3, version_1_0));
    }

テスト結果

ちゃんと動くっぽいですね。

これで Equals と GetHashCode の実装は楽できそうです。

式木おもしろい!

式木とてもおもしろいですね。メタプログラミングって面白いなあと思います。

使い方を誤ると魔物が生まれそうですが、上手に使えばすごく役立ちそうです。(ぼくの頭ではなかなかアイデアが浮かびませんが…

参考にさせていただきました

大変参考にさせていただきました。

2012年7月23日月曜日

[C#]エンティティのEqualsとGetHashCode

しばらくの間、曖昧な理解のまま Equals と GetHashCode を実装していたので、反省。

まず、Equals と GetHashCode の実装については、以下の記事が参考になります。

概ね理解しているつもりだったんですが、今日ハッシュのコレクションを扱うときに、コレクション内のある要素を削除(Remove)しようとしても、削除できない(Contains(item) が false になってしまう)、というバグが出たので調べていたら、こちらの記事に行き当たりました。

  1. 2 つのオブジェクトが等しい (== 演算子の定義によって等しい) 場合、それらのオブジェクトからは同じハッシュ値が生成されなければならない。同じハッシュ値が生成されない場合、コンテナからオブジェクトを探しだすためのキーとしてハッシュコードを使用することができない。
  2. 任意のオブジェクト A に対して、A.GetHashCode() は各インスタンス毎に異なる値でなければならない。A のどんなメソッドが呼ばれたとしても、A.GetHashCode() は常に同じ値を返さなければならない。これによって、オブジェクトが格納されている正しいバケツを常に見つけだすことができる。
  3. ハッシュ関数は任意に入力された複数の整数値に対し、ランダム分布となる返り値を返す必要がある。この性質により、ハッシュベースのコンテナへ効率的にオブジェクトを格納できる。

この実装ポリシーのうち、2番を思いっきり無視していました。つまり、ハッシュのコレクションを生成したあとに、コレクション内のある要素の GetHashCode の演算に利用してるプロパティが変化して、ハッシュ値が変わってしまったようです。

また、以下の様な記事もありました。

 

エンティティにおける、Equals と GetHashCode の実装

今回このハッシュのコレクションというのは、いわいるエンティティのコレクションだったわけですが、その実装が以下のようなもの。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return 
            other != null && 
            FirstName == other.FirstName && 
            LastName == other.LastName;
    }

    public override int GetHashCode()
    {
        return 
            (FirstName ?? string.Empty).GetHashCode() ^ 
            (LastName ?? string.Empty).GetHashCode();
    }
}

テーブルのほうはこんな感じです。

CREATE TABLE Users (
    Id INT IDENTITY PRIMARY KEY,
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    UNIQUE (FirstName, LastName)
)

Id が人工キーで、スキーマ上のメインの識別子になります。自然キーとして FirstName と LastName が利用できる(実際には名前は被るので使えないと思いますが…例です)ので、FirstName と LastName に一意キー制約を付けてあります。

この Equals 及び GetHashCode の実装では、以下のガイドラインの推奨に従って、人工キーではなく自然キーによる等価比較の実装を行なっています。(ちなみに ORM は NHibernate です

ビジネスキーの等価性 を使って、 equals() と hashCode() を実装することをお勧めします。ビジネスキーの等価性とは、 equals() メソッドが、ビジネスキー、つまり現実の世界においてインスタンスを特定するキー(自然 候補キー) を形成するプロパティだけを比較することを意味します。

このとき、FirstName と LastName が変化する場合、上記のような実装だとハッシュ値が変わってしまってポリシー違反を起こします。

なので、FirstName と LastName が変化する属性としての側面をもつ場合は、Equals と GetHashCode の実装に使うとマズイということになります。(FirstName と LastName が不変ならオッケーなんですが

このような場合は、素直に変化しない(はずの) Id を用いたほうがよさそうです。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return
            other != null &&
            Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

本来なら自然キーでの実装のほうが、なにかと扱いやすいのですが仕方ないですね。

ちなみにこの例でも、Id のセッタが公開されてしまっているので、キチンと実装する場合は、Id が不変になるように工夫したほうがよさそうです。

2012年7月18日水曜日

[C#]Enumに振る舞いを

Enumにプロパティとかメソッドとか、豊富な振る舞いを持たせたい時どうすればいいんだろうという疑問です。

拡張メソッドを使う

public enum AircraftType
{
    Fighter,
    Attacker,
    Bomber
}

public static class AircraftTypeHelper
{
    public static string DisplayName(this AircraftType value)
    {
        return _displayNames[value];
    }

    private readonly static IDictionary<AircraftType, string> _displayNames = new Dictionary<AircraftType, string>()
    {
        { AircraftType.Fighter, "戦闘機" },
        { AircraftType.Attacker, "攻撃機" },
        { AircraftType.Bomber, "爆撃機" }
    };
}
 string displayName = AircraftType.Fighter.DisplayName();

こんな感じに拡張メソッドを使うと、プロパティは無理だけど、それなりに振る舞いをつけることができます。

素直にクラスで実装する

public class AircraftType
{
    private AircraftType(AircraftTypeValue value)
    {
        value = value;
    }

    private enum AircraftTypeValue
    {
        Fighter,
        Attacker,
        Bomber
    }

    public static readonly AircraftType Fighter = new AircraftType(AircraftTypeValue.Fighter);
    public static readonly AircraftType Attacker = new AircraftType(AircraftTypeValue.Attacker);
    public static readonly AircraftType Bomber = new AircraftType(AircraftTypeValue.Bomber);

     private readonly AircraftTypeValue _value;

    public override bool Equals(object obj)
    {
        var other = obj as AircraftType;
        return other != null && _value == other._value;
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

こんな感じでシングルトンで実装する方法です。Equals とか ToString なんかの基本的なメソッドの実装は内部に隠してある Enum に丸投げ。

これならプロパティでもなんでも追加できます。(当たり前だけど...)

    public string DisplayName
    {
        get { return _displayNames[_value]; }
    }

    private readonly static IDictionary<AircraftTypeValue, string> _displayNames = new Dictionary<AircraftTypeValue, string>()
    {
        { AircraftTypeValue.Fighter, "戦闘機" },
        { AircraftTypeValue.Attacker, "攻撃機" },
        { AircraftTypeValue.Bomber, "爆撃機" }
    };
    string displayName = AircraftType.Fighter.DisplayName;

どっちがいいんだろう

拡張メソッドを使う方はまんま Enum を使うので手軽ですね。でも拡張メソッド以上のことはできないので、複雑な振る舞いを持たせたりするのには向いていないですね。

シングルトンでの実装は、自由にプロパティでもメソッドでも追加できるので柔軟性はあります。でも、実装がちょっと面倒です。

また、Enum をそのまま使うと FlagsAttributes でビット演算できたりして重宝することもあります。

個人的には後者(シングルトン)を使うことが多いのですが、振る舞いなんかがあまりない純粋な列挙値だったら普通に Enum を使います。あと、ビット演算したいときとか…

ケース・バイ・ケースなのかな?

2011年12月3日土曜日

[C#]CompareToの実装を楽にする


最近ちまちまとIComparableを実装していた。例えばこんなクラスがあったら、

public class Version
{
    public Version()
    {
    }
    
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
}

こんな風にIComparableを実装していく。

public class Version : IComparable, IComparable<Version>
{
    public Version()
    {
    }
 
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
 
    public int CompareTo(object obj)
    {
        return CompareTo(obj as Version);
    }
 
    public int CompareTo(T other)
    {
        if (other == null)
            return 1;

        int majorCompared = Major.CompareTo(other.Major);
        if (majorCompared != 0)
            return majorCompared;

        int minorCompared = Minor.CompareTo(other.Minor);
        if (minorCompared != 0)
            return minorCompared;

        int buildCompared = Build.CompareTo(other.Build);
        if (buildCompared != 0)
            return buildCompared;

        return Revision.CompareTo(other.Revison);
    }
}

3つぐらい実装したところで、面倒すぎて死ぬかと思った。しかも、比較対象が参照型だと、nullの判定まで必要でさらに面倒くさい。

そこで、ちょっと考えることにした。

どうしよう


どうせほとんどのクラスが、内部フィールドの単純な比較の合成なんだから、宣言的にそういうことをやってくれる汎用クラスを一つ作れば良い。

具体的には以下のような使い方をしたい。


[TestFixture]
public class ComplexComparerTest
{
    [Test]
    public void Comparer_Test()
    {
        var comparer = new ComplexComparer<Version>();
        comparer
            .Member(v => v.Major)
            .Member(v => v.Minor)
            .Member(v => v.Build)
            .Member(v => v.Revision)
            ;
  
        var ver_1_0_0_0 = new Version(1, 0, 0, 0);
        var ver_2_0_0_0 = new Version(2, 0, 0, 0);

        Assert.IsTrue(0 < comparer.Compare(ver_1_0_0_0, ver_2_0_0_0));
        Assert.IsTrue(0 > comparer.Compare(ver_2_0_0_0, ver_1_0_0_0));
    }
}

ラムダ式で比較に利用するメンバを設定するメソッド(Memberメソッド)を用意し、設定した順に比較が行われるようにしたい。


実装しよう

というわけで実装する。適当にメンバを用意する。


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        // ここ実装
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        // ここ実装
        return 0;
    }
}

比較処理を順番に実行していけばいいんだから、比較デリゲートのリストを持つようにして、CompareToの中身を実装してしまおう。


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    private readonly IList<Comparison<T>> _comparisons = new List<Comparison<T>>();

    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        // あとで実装
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }
}

あとはMemberメソッドで、比較デリゲートが追加されるようにするだけだ。


public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        var compiled = expression.Compile();

        _comparisons.Add((x, y) => 
        {
            T xMember = compiled(x);
            T yMember = compiled(y);
   
            if (xMember != null)
                return xMember.CompareTo(yMember);
   
            if (yMember != null)
                return yMember.CompareTo(xMember) * -1;
   
            return 0;
        });

        return this;
    }

Compareメソッドにnullの場合の判定も入れておこう。


public int Compare(T x, T y)
    {
        if (x == null || y == null)
        {
            if (x != null) return 1;
            if (y != null) return -1;
            return 0;
        }
  
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }



かんせい


public class ComplexComparer<T> : IComparer, IComparer<T>
{
    private readonly IList<Comparison<T>> _comparisons = new List<Comparison<T>>();

    public ComplexComparer<T> Member<TMember>(Expression<Func<T, TMember>> expression)
        where TMember : IComparable
    {
        var compiled = expression.Compile();

        _comparisons.Add((x, y) => 
        {
            T xMember = compiled(x);
            T yMember = compiled(y);
   
            if (xMember != null)
                return xMember.CompareTo(yMember);
   
            if (yMember != null)
                return yMember.CompareTo(xMember) * -1;
   
            return 0;
        });
        
        return this;
    }

    public int Compare(object x, object y)
    {
        return Compare((T)x, (T)y);
    }

    public int Compare(T x, T y)
    {
        if (x == null || y == null)
        {
            if (x != null) return 1;
            if (y != null) return -1;
            return 0;
        }
  
        foreach (var comparison in _comparisons)
        {
            var compared = comparison(x, y);
            if (compared != 0)
                return compared;
        }

        return 0;
    }
}

使ってみる

Versionクラスに実装してみる。

public class Version : IComparable, IComparable<Version>
{
    public Version()
    {
    }
 
    public Version(int major, int minor, int build, int revision)
    {
        Major = major;
        Minor = minor;
        Build = build;
        Revision = revision;
    }
 
    public int Major { get; set; }
    public int Minor { get; set; }
    public int Build { get; set; }
    public int Revision { get; set; }
 
    private static readonly ComplexComparer<Version> _comparer = 
        new ComplexComparer<Version>()
            .Member(v => v.Major)
            .Member(v => v.Minor)
            .Member(v => v.Build)
            .Member(v => v.Revision);
 
    public int CompareTo(object obj)
    {
        return CompareTo(obj as Version);
    }
 
    public int CompareTo(T other)
    {
        return _comparer.Compare(this, other);
    }
}

とてもすっきりしたし楽になった。これなら間違いも少なくなるだろう。めでたしめでたし。

まとめ


  • 内部の比較デリゲートに詰める処理をアレンジすればいろいろバリエーションが広がる。
  • Expressionは便利
  • 宣言的な書き方は楽だし間違いが少ない。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...