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は便利
  • 宣言的な書き方は楽だし間違いが少ない。

0 件のコメント:

コメントを投稿

TFT 10.14 Peeba Comp

こちらのガイドの自分用まとめです。 https://www.reddit.com/r/CompetitiveTFT/comments/hraunp/tft_1014_break_the_meta_new_peeba_comp_set_35/ 難しいですが完成すると非常に強く、プレ...