Question

和和和 on Sat, 27 Jul 2013 15:02:28


まずはコードを見てからの方が説明しやすいと思います。

using System;
using System.Collections.Generic;
using System.Diagnostics;
public abstract class AKey<T> {
    public abstract Boolean KeyEquals(T other);
    public abstract Int32 KeyGetHashCode();
}
public class Key:AKey<Key>,IEquatable<Key> {
    private Int32 K0;
    public Key(Int32 K0) {
        this.K0=K0;
    }
    public bool Equals(Key other) {
        return this.KeyEquals(other);
    }
    public override bool Equals(object obj) {
        var other=obj as Key;
        if(other==null) return false;
        return this.Equals(other);
    }
    public override int GetHashCode() {
        return this.KeyGetHashCode();
    }
    public override Boolean KeyEquals(Key other) {
        return this.K0.Equals(other.K0);
    }
    public override Int32 KeyGetHashCode() {
        return this.K0.GetHashCode();
    }
}
public class A:Key,IEquatable<A> {
    public Int32 A0;
    public A(Int32 K0,Int32 A0):base(K0) {
        this.A0=A0;
    }
    public bool Equals(A other) {
        if(!this.Equals(other as Key)) return false;
        return this.A0.Equals(other);
    }
    public override bool Equals(object obj) {
        var other=obj as A;
        if(other==null) return false;
        return this.Equals(other);
    }
    public override int GetHashCode() {
        var r=base.GetHashCode();
        r<<=1; r^=this.A0.GetHashCode();
        return r;
    }
}
public class B:A,IEquatable<B> {
    public Int32 B0;
    public B(Int32 K0,Int32 A0,Int32 B0)
        : base(A0,K0) {
        this.B0=B0;
    }
    public bool Equals(B other) {
        if(!this.Equals(other as A)) return false;
        return this.B0.Equals(other);
    }
    public override bool Equals(object obj) {
        var other=obj as B;
        if(other==null) return false;
        return this.Equals(other);
    }
    public override int GetHashCode() {
        var r=base.GetHashCode();
        r<<=1; r^=this.B0.GetHashCode();
        return r;
    }
}
public sealed class PrimaryKeyEqualityComparer<T>:EqualityComparer<T> where T:AKey<T> {
    public override bool Equals(T x,T y) {
        return x.KeyEquals(y);
    }
    public override int GetHashCode(T obj) {
        return obj.KeyGetHashCode();
    }
}
class Program {
    static void Main() {
        //B重複しない
        {
            var array=new Key[]{
                new B(0,0,0),
                new B(0,0,1),
            };
            //配列要素インスタンスはIEqutable<B>.Equalsが使われているこれはK0,A0,B0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==array.Length);
        }
        //B重複する
        {
            var array=new Key[]{
                new B(0,0,0),
                new B(0,0,0),
            };
            //配列要素インスタンスはIEqutable<B>.Equalsが使われているこれはK0,A0,B0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==1);//重複しているので1つにまとめられる
        }
        //A重複しない
        {
            var array=new Key[]{
                new A(0,0),
                new A(0,1),
            };
            //配列要素インスタンスはIEqutable<A>.Equalsが使われているこれはK0,A0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==array.Length);
        }
        //A重複する
        {
            var array=new Key[]{
                new A(0,0),
                new A(0,0),
            };
            //配列要素インスタンスはIEqutable<A>.Equalsが使われているこれはK0,A0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==1);//重複しているので1つにまとめられる
        }
        //Key重複しない
        {
            var array=new Key[]{
                new Key(0),
                new Key(1),
            };
            //配列要素インスタンスはIEqutable<Key>.Equalsが使われているこれはK0,A0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==array.Length);
        }
        //Key重複する
        {
            var array=new Key[]{
                new Key(0),
                new Key(0),
            };
            //配列要素インスタンスはIEqutable<Key>.Equalsが使われているこれはK0,A0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array);
            Debug.Assert(h.Count==1);//重複しているので1つにまとめられる
        }
        //ここからが問題。A,Bのインスタンスで比較基準をKey.Equals,Key.GetHashCodeを使うことは出来ないか?
        {
            //↓例えばこのデータはKeyを基準にすると重複している
            var array=new Key[]{
                new A(0,0),
                new A(0,1),
            };
            //配列要素インスタンスはIEqutable<A>.Equalsが使われているこれはK0,A0全体で一致しているか判定しているため重複は除かれない
            var h=new HashSet<Key>(array,new PrimaryKeyEqualityComparer<Key>());
            Debug.Assert(h.Count==1);
        }
        //AKeyを継承したもの、していないもを共通メソッド名でオーバーロードで処理したい
        {
            //↓例えばこのデータはKeyを基準にすると重複している
            var array=new Key[]{
                new A(0,0),
                new A(0,1),
            };
            重複チェックオーバーロード(array);//コンパイルエラー。オーバーロードできない。
        }
        {
            //↓例えばこのデータはKeyはないので全体の値の集合として重複している
            var array=new[] { 0,0 };
            重複チェックオーバーロード(array);//コンパイルエラー。オーバーロードできない。
        }
    }
    //コンパイルエラー。オーバーロードできない。
    static void 重複チェックオーバーロード<T>(T[] array) where T:AKey<T> {
        var h=new HashSet<T>(array,new PrimaryKeyEqualityComparer<T>());
        Debug.Assert(h.Count==array.Length);
    }
    //コンパイルエラー。オーバーロードできない。
    static void 重複チェックオーバーロード<T>(T[] array) {
        var h=new HashSet<T>(array);
        Debug.Assert(h.Count==array.Length);
    }
}

これをコンソールプロジェクトの1つのソースに張り付ければ現在問題になっているコンパイルエラーが出ます。

その部分を取り除けばそれ以外は動作します。

つまりクラスB.EqualsはメンバB0,A0,K0が一致しているかどうかを評価しHashSet<>で重複を除く処理をやっているのです。

これをクラスBのクラスKeyのメンバK0のみで一致しているか評価しHashSet<>で重複を除きたいのです。

「重複チェックオーバーロード」という名前を変えれば希望の動作になりますが使い勝手がよくありません。

オーバーロードではなく単一のメソッドでよいので内部で処理を切り替えるという方法でも構いません、よひ方法はないものでしょうか?


Sponsored



Replies

佐祐理 on Sat, 27 Jul 2013 22:49:08


私なら

public abstract class AKey{
  public abstract int Key{ get; }
}
public KeyEquatableComperer: IEqutableComparer<AKey>{
  punlic override bool Equals( AKey x, AKey y ){
    x.Key.Equals( y.Key );
  }
  public override int GetHashCode( AKey obj ){
    obj.Key.GetHashCode();
  }
}

みたいにします。

AKey<T>のようにgenericにする必要はないのでは?

また、PrimaryKeyEqualityComparer→AKey<T>→Keyと見通しが悪く感じます。こうすれば、重複チェックオーバーロードはgenericにする必要がなくなるので書けると思います。

それから、キー比較とオブジェクト比較を同一のオーバーローで記述できたとして、一見見栄えはいいのかもしれませんが、どちらで比較しているかわかりづらいのでメソッド名を分けたほうがいいと思います。

最後に蛇足ですが、C#はコンストラクターの型推論が行われなく明示的に型名を記述する必要があるため、コンストラクタを呼び出すstatic methodを用意するとコードが読みやすくなります。(static methodなら型推論されるので型パラメーターの記述を省略できます。)

和和和 on Mon, 29 Jul 2013 12:13:39


ちょっと自分がやりたいことを整理しました。それでもうまく説明できないかもしれません。

  1. 先に例示したKey,A,BクラスはEntityFrameworkデザイナの.edmxファイルから.tt(t4 template)から生成したつもりの定義です。だからここのコードが増大することは余り問題ではありません。
  2. Aインスタンスはオブジェクト全体の等価性のためにIEqutable<A>を実装しています。BはAを継承していますが同様です。
  3. A,Bは主キーを持ちます。その主キーを表すためにKeyを定義し、継承しています。これは継承ではなくメンバに持たせることも実装としてはありだと思います。
  4. A,Bは関係代数として扱う場合重複は除かれます。関係代数のためのXクラスは既に別に実装してあります。
  5. A,Bは主キーが重複する場合例外か検出をしたいです。その機能実現のために試行錯誤でPrimaryKeyEqualityCompare<T>,AKey<T>クラスなどを用意しました。これはなくても他にどんな方法でもいいです。但しスループットを大幅に下げるような動的な機能は避けたいです。ILなどを使って初期化に時間がかかってもスループットが向上するなら有です。もちろんスマートに言語仕様内できれいに決まればそれに越したことはないです。
  6. 5.の機能実現のためにクラス生成する.ttファイルを書き直すこともOKです。

佐祐理さんのいわれる

AKey<T>のようにgenericにする必要はないのでは?

という構造を変えるのはもちろんいいです。しかし関係代数のためのXクラスではどうもうまくいきません。それはキーを含むエンティティ型ここではA,Bの集合を作るときは主キーの制約を守らねばなりませんが、それ以外の射影した集合はタプル全体で重複がなければいいもんですから素直に実装するとメソッドが2倍になっていまします。

たとえばSet<T>.Select<TResult>(Func<T,TResult>selector)

というSQL,LINQっぽい射影機能があるのですがこの結果Set<T>はTResult型のオブジェクトの重複は除かれたえた状態の結果を保持します。

こらは主キーのない結果の場合はそれでいいです。

しかし主キーがある場合の結果は

Adata.Select(p=>new A(Key:0,Data:"DATA"))

とやるとA.Keyで2つ以上0は不正の出例外を発生させなければなりません。

このXクラスれをどうやってうまく実装すればよいか悩んでいるのです。

さゆりさんが定義されたAKey<T>をGenericにしなかったとしてどうやって実装できたのか

キー比較、オブジェクト比較のメソッド名を別にするとして現状HahSet<T>を使って重複除去、重複検出をしている限りそれは無理だと思います。

別の案としてHashSetに代わる重複検出用のクラスを作ればまた変わるかもれませんのでそれを考えています。

佐祐理 on Mon, 29 Jul 2013 12:38:30


何を言っているのか読んでもさっぱりわかりませんでした。

私の挙げたAKeyクラスでなく、AKey<T>が必要な理由を説明してみてください。このクラスは比較さえできればよく、T型の比較に固執する理由がわかりません。

重複が許されないというのもよくわかりません。それぞの型のObject.Equals()を適切に実装するだけでは? 主キーだけ見たければそう実装し、メンバーすべてを比較するのであればそうするだけです。

和和和 on Tue, 30 Jul 2013 13:42:29


うまく説明できなくて申し訳ありません。

私の挙げたAKeyクラスでなく、AKey<T>が必要な理由を説明してみてください。

というのはAKey<>クラスにはメンバを比較するメソッドKeyEqualsが存在するのですがGenericではないAKeyで定義するとそのAKeyを扱うクラスがKeyEqualsを呼び出すためにキャストなしで呼び出したいと考えたからです。

AKey<PrimaryKey>を継承してAクラスを作るか、AクラスのメンバにAKey<PrimaryKey>を持つにしてもHashSet<>でPrimaryKeyのメンバで比較させることは可能ですが、

AKey<PrimaryKey>を継承しない、AKey<PrimaryKey>をメンバに持たないクラスはHashSet<>でPrimaryKeyのメンバで比較させない、という使い分けをオーバーロードかif文で選びたいのです。


佐祐理 on Wed, 31 Jul 2013 00:34:20


というのはAKey<>クラスにはメンバを比較するメソッドKeyEqualsが存在するのですがGenericではないAKeyで定義するとそのAKeyを扱うクラスがKeyEqualsを呼び出すためにキャストなしで呼び出したいと考えたからです。

それについては最初にコメントしたように設計の問題です。AKeyがキーによる比較を提供したいにもかかわらずAKeyクラス内で比較を実装せず、派生クラスに任せているからです。

ところでコードを見ていて別の問題に気付きました。Dictionary<TKey, TValue>とIEquatable<T>にも書いたのですが、IEquatable<T>はGetHashCode()を提供しませんが、DictionaryやHashSetはGetHashCode()を必要とします。そのため、Object.GetHashCode()を使います。
その結果、HashSet<Key>とした場合、IEquatable<Key>で比較する前に(Key.GetHashCode()ではなく)A.GetHashCode()やB.GetHashCode()でハッシュされます。こうなると、キーで比較したかったにもかかわらずメンバー全体の値でハッシュされるため、たとえキーが一致していても重複しているとは判断されません。

結局、クラスに対してEquals() / GetHashCode()は1種類だけ定義し、それとは異なる比較を行いたい場合は、IEqualityComparer<T>を使用することをお勧めします。

和和和 on Wed, 31 Jul 2013 01:57:51


結局、クラスに対してEquals() / GetHashCode()は1種類だけ定義し、それとは異なる比較を行いたい場合は、IEqualityComparer<T>を使用することをお勧めします。

この言葉でひらめき実装してみました。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
static partial class アイデア1 {
    class 抽象Class:IEquatable<抽象Class> {//抽象、だけどプログラムのテストのためabstractはつけていない
        public class EqualityComparer:IEqualityComparer<抽象Class> {
            public bool Equals(抽象Class x,抽象Class y) {
                return x.ID.Equals(x.ID);
            }
            public int GetHashCode(抽象Class obj) {
                return obj.ID.GetHashCode();
            }
        }
        public static readonly EqualityComparer Default=new EqualityComparer(); 
        public readonly Int32 ID;//主キーを表す
        public readonly Int32 値1;
        public 抽象Class(Int32 ID,Int32 値1){
            this.ID=ID;
            this.値1=値1;
        }
        public bool Equals(抽象Class other) {
            if(this.GetType()!=other.GetType()) return false;
            if(!this.ID.Equals(other.ID)) return false;
            if(!this.値1.Equals(other.値1)) return false;
            return true;
        }
        public override bool Equals(object obj) {
            var other=obj as 抽象Class;
            if(other==null) return false;
            return this.Equals(other);
        }
        public override int GetHashCode() {
            var r=0;
            r<<=1; r^=this.ID;
            r<<=1; r^=this.値1;
            return r;
        }
    }
    class 具象Class:抽象Class,IEquatable<具象Class>{
        public readonly Int32 値2;
        public 具象Class(Int32 ID,Int32 値1,Int32 値2)
            : base(ID,値1) {
                this.値2=値2;
        }
        public bool Equals(具象Class other) {
            if(!base.Equals(other)) return false;
            if(!this.値2.Equals(other.値2))return false;
            return true;
        }
        public override bool Equals(object obj) {
            var other=obj as 具象Class;
            if(other==null) return false;
            return this.Equals(other);
        }
        public override int GetHashCode() {
            var r=base.GetHashCode();
            r<<=1; r^=this.値2;
            return r;
        }
    }
    static void 重複除き行数_主キー重複除き主キー数<T>(Int32 タプル重複除き数,Int32 主キー重複除き数,params T[] array){
        {
            var h=new HashSet<T>(array);
            Debug.Assert(h.Count==タプル重複除き数);
        }
        {
            var Default=typeof(T).GetField("Default",BindingFlags.Static|BindingFlags.Public|BindingFlags.FlattenHierarchy).GetValue(null) as IEqualityComparer<T>;
            var h=new HashSet<T>(Default);
            foreach(var a in array) {
                h.Add(a);
            }
            Debug.Assert(h.Count==主キー重複除き数);
        }
    }
    static void 抽象Classテスト() {
        {
            var array=new[]{
                new 抽象Class(ID :0,値1 :0),
                new 抽象Class(ID :0,値1 :0)
            };
            重複除き行数_主キー重複除き主キー数(1,1,array);
        }
        {
            var array=new[]{
                new 抽象Class(ID :0,値1 :0),
                new 抽象Class(ID :0,値1 :1)
            };
            重複除き行数_主キー重複除き主キー数(2,1,array);
        }
        {
            var array=new[]{
                new 抽象Class(ID :0,値1 :0),
                new 抽象Class(ID :1,値1 :0)
            };
            重複除き行数_主キー重複除き主キー数(2,2,array);
        }
    }
    static void 具象Classテスト() {
        {
            var array=new[]{
                new 具象Class(ID :0,値1 :0,値2 :0),
                new 具象Class(ID :0,値1 :0,値2 :0)
            };
            重複除き行数_主キー重複除き主キー数(1,1,array);
        }
        {
            var array=new[]{
                new 具象Class(ID :0,値1 :0,値2 :0),
                new 具象Class(ID :0,値1 :0,値2 :1)
            };
            重複除き行数_主キー重複除き主キー数(2,1,array);
        }
        {
            var array=new[]{
                new 具象Class(ID :0,値1 :0,値2 :0),
                new 具象Class(ID :1,値1 :0,値2 :1)
            };
            重複除き行数_主キー重複除き主キー数(2,2,array);
        }
    }
    static void Main() {
        抽象Classテスト();
        具象Classテスト();
    }
}

これで希望どおりの動作になりました。
クラスのメンバにIEqualityComparer<>のフィールドを持つ構造が良いかどうかはわかりませんがこれでいいと思います。

ありがとうございました。