12

この質問は、タプルに関する議論から生まれました。

タプルが持つべきハッシュコードについて考え始めました。KeyValuePair クラスをタプルとして受け入れるとどうなるでしょうか? GetHashCode() メソッドをオーバーライドしないため、おそらく「子」のハッシュ コードを認識しないでしょう...したがって、ランタイムは Object.GetHashCode() を呼び出しますが、これは認識されません。実際のオブジェクト構造。

次に、GetHashCode() と Equals() がオーバーロードされているため、参照型の 2 つのインスタンスを実際に Equal にすることができます。そして、それらをタプルの「子」として使用して、辞書を「ごまかす」。

しかし、うまくいきません!ランタイムはどうにかしてタプルの構造を理解し、クラスのオーバーロードされた GetHashCode を呼び出します!

それはどのように機能しますか?Object.GetHashCode() によって行われた分析は何ですか?

複雑なキーを使用する場合、悪いシナリオでパフォーマンスに影響を与える可能性はありますか? (おそらく、不可能なシナリオ...しかし、それでも)

次のコードを例として考えてみましょう。

namespace csharp_tricks
{
    class Program
    {
        class MyClass
        {
            int keyValue;
            int someInfo;

            public MyClass(int key, int info)
            {
                keyValue = key;
                someInfo = info;
            }

            public override bool Equals(object obj)
            {
                MyClass other = obj as MyClass;
                if (other == null) return false;

                return keyValue.Equals(other.keyValue);
            }

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

        static void Main(string[] args)
        {
            Dictionary<object, object> dict = new Dictionary<object, object>();

            dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 1), 1), 1);

            //here we get the exception -- an item with the same key was already added
            //but how did it figure out the hash code?
            dict.Add(new KeyValuePair<MyClass,object>(new MyClass(1, 2), 1), 1); 

            return;
        }
    }
}

更新以下の回答で説明されているように、これについての説明を見つけたと思います。その主な成果は次のとおりです。

  • キーとそのハッシュ コードには注意してください :-)
  • 複雑な辞書キーの場合、Equals() と GetHashCode() を正しくオーバーライドする必要があります。
4

6 に答える 6

13

変更可能なクラスで GetHashcode() と Equals() をオーバーライドしないでください。変更できないクラスまたは構造体でのみオーバーライドしてください。そうしないと、キーとして使用されるオブジェクトを変更すると、ハッシュ テーブルが適切に機能しなくなります (変更できなくなります)。キーオブジェクトが変更された後にキーに関連付けられた値を取得します)

また、ハッシュテーブルは、キーオブジェクト自体を識別子として使用するオブジェクトを識別するためにハッシュコードを使用しません。ハッシュテーブルにエントリを追加するために使用されるすべてのキーが異なるハッシュコードを返す必要はありませんが、そうすることが推奨されます。大いに苦しむ。

于 2008-09-19T15:26:38.323 に答える
3

以下は、Quad タプル (内部に 4 つのタプル コンポーネントを含む) の適切なハッシュと等値の実装です。このコードにより、HashSet と辞書でこの特定のタプルが適切に使用されるようになります。

この件に関する詳細 (ソース コードを含む) は、こちら.

uncheckedキーワードの使用(オーバーフローを回避するため) と、obj が null の場合に NullReferenceException をスローすることに注意してください (基本メソッドで必要)

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj))
        throw new NullReferenceException("obj is null");
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != typeof (Quad<T1, T2, T3, T4>)) return false;
    return Equals((Quad<T1, T2, T3, T4>) obj);
}

public bool Equals(Quad<T1, T2, T3, T4> obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return Equals(obj.Item1, Item1)
        && Equals(obj.Item2, Item2)
            && Equals(obj.Item3, Item3)
                && Equals(obj.Item4, Item4);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = Item1.GetHashCode();
        result = (result*397) ^ Item2.GetHashCode();
        result = (result*397) ^ Item3.GetHashCode();
        result = (result*397) ^ Item4.GetHashCode();
        return result;
    }
}
public static bool operator ==(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return Equals(left, right);
}


public static bool operator !=(Quad<T1, T2, T3, T4> left, Quad<T1, T2, T3, T4> right)
{
    return !Equals(left, right);
}
于 2008-09-30T17:23:11.063 に答える
2

object.GetHashCode の仕組みの詳細については、Brad Abrams によるこの投稿と Brian Grunkemeyer によるコメントを参照してください。また、Ayande のブログ投稿の最初のコメントもご覧ください。フレームワークの現在のリリースがまだこれらのルールに従っているのか、それともブラッドが暗示したように実際に変更されたのかはわかりません。

于 2008-09-19T15:36:32.623 に答える
1

どうやら私は今、手がかりを持っているようです。

KeyValuePair は参照型だと思っていましたが、そうではなく、構造体です。そのため、ValueType.GetHashCode() メソッドを使用します。MSDN には、「派生型の 1 つ以上のフィールドを使用して戻り値が計算されます」と記載されています。

「タプルプロバイダー」として実際の参照型を使用する場合は、辞書 (または自分自身...) をごまかすことになります。

using System.Collections.Generic;

namespace csharp_tricks
{
    class Program
    {
        class MyClass
        {
            int keyValue;
            int someInfo;

            public MyClass(int key, int info)
            {
                keyValue = key;
                someInfo = info;
            }

            public override bool Equals(object obj)
            {
                MyClass other = obj as MyClass;
                if (other == null) return false;

                return keyValue.Equals(other.keyValue);
            }

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

        class Pair<T, R>
        {
            public T First { get; set; }
            public R Second { get; set; }
        }

        static void Main(string[] args)
        {
            var dict = new Dictionary<Pair<int, MyClass>, object>();

            dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 2) }, 1);

            //this is a pair of the same values as previous! but... no exception this time...
            dict.Add(new Pair<int, MyClass>() { First = 1, Second = new MyClass(1, 3) }, 1);

            return;
        }
    }
}
于 2008-09-19T16:44:28.497 に答える
0

私は本のリファレンスをもう持っていないので、確認するためにそれを見つける必要がありますが、デフォルトのベースハッシュはオブジェクトのすべてのメンバーをハッシュしただけだと思いました. CLR の仕組みが原因でそれらにアクセスできたので、彼らが持っていたようにあなたが書くことができるものではありませんでした。

それは完全に、私が簡単に読んだことの記憶からのものです。

編集:本はMS PressのInside C#でした。表紙にのこぎり刃が付いたもの。著者は、CLR でどのように実装されているか、言語がどのように MSIL に変換されているかなどについて、かなりの時間を費やして説明しました。など。その本を見つけることができれば、それは悪い読書ではありません。

編集:次のように見える場合はリンクを形成します

Object.GetHashCode() は、System.Object クラスの内部フィールドを使用してハッシュ値を生成します。作成された各オブジェクトには一意のオブジェクト キーが割り当てられ、作成時に整数として格納されます。これらのキーは 1 から始まり、任意のタイプの新しいオブジェクトが作成されるたびに増加します。

うーん、オブジェクトをハッシュ キーとして使用する場合は、独自のハッシュ コードをいくつか記述する必要があると思います。

于 2008-09-19T15:21:47.477 に答える
-1

したがって、おそらく「子」のハッシュコードを認識しないでしょう。

あなたの例はそうではないことを証明しているようです:-)キーMyClassと値のハッシュコード1は両方ので同じですKeyValuePairKeyKeyValuePairの実装では、独自のハッシュコードとValue独自のハッシュコードの 両方を使用する必要があります

上に移動すると、辞書クラスは一意のキーを必要とします。それは物事を理解するために各キーによって提供されるハッシュコードを使用しています。ランタイムはを呼び出していませんがObject.GetHashCode()、指定したインスタンスによって提供されるGetHashCode()実装を呼び出していることに注意してください。

より複雑なケースを考えてみましょう。

public class HappyClass
{

    enum TheUnit
    {
        Points,
        Picas,
        Inches
    }

    class MyDistanceClass
    {
        int distance;
        TheUnit units;

        public MyDistanceClass(int theDistance, TheUnit unit)
        {
            distance = theDistance;

            units = unit;
        }
        public static int ConvertDistance(int oldDistance, TheUnit oldUnit, TheUnit newUnit)
        {
            // insert real unit conversion code here :-)
            return oldDistance * 100;
        }

        /// <summary>
        /// Figure out if we are equal distance, converting into the same units of measurement if we have to
        /// </summary>
        /// <param name="obj">the other guy</param>
        /// <returns>true if we are the same distance</returns>
        public override bool Equals(object obj)
        {
            MyDistanceClass other = obj as MyDistanceClass;
            if (other == null) return false;

            if (other.units != this.units)
            {
                int newDistance = MyDistanceClass.ConvertDistance(other.distance, other.units, this.units);
                return distance.Equals(newDistance);
            }
            else
            {
                return distance.Equals(other.distance);
            }


        }

        public override int GetHashCode()
        {
            // even if the distance is equal in spite of the different units, the objects are not
            return distance.GetHashCode() * units.GetHashCode();
        }
    }
    static void Main(string[] args)
    {

        // these are the same distance... 72 points = 1 inch
        MyDistanceClass distPoint = new MyDistanceClass(72, TheUnit.Points);
        MyDistanceClass distInch = new MyDistanceClass(1, TheUnit.Inch);

        Debug.Assert(distPoint.Equals(distInch), "these should be true!");
        Debug.Assert(distPoint.GetHashCode() != distInch.GetHashCode(), "But yet they are fundimentally different values");

        Dictionary<object, object> dict = new Dictionary<object, object>();

        dict.Add(new KeyValuePair<MyDistanceClass, object>(distPoint, 1), 1);

        //this should not barf
        dict.Add(new KeyValuePair<MyDistanceClass, object>(distInch, 1), 1);

        return;
    }

}

基本的に...私の例の場合、同じ距離にある2つのオブジェクトが、Equalsに対して「true」を返すが、異なるハッシュコードを返すようにします。

于 2008-09-19T15:48:19.240 に答える