Використання поля об’єкта як загального ключа словника


120

Якщо я хочу використовувати об'єкти як ключі для a Dictionary, які методи мені знадобляться, щоб змусити їх порівнювати певним чином?

Скажіть, у мене клас aa, який має властивості:

class Foo {
    public string Name { get; set; }
    public int FooID { get; set; }

    // elided
} 

І я хочу створити:

Dictionary<Foo, List<Stuff>>

Я хочу, щоб Fooоб’єкти з тим самим FooIDвважалися однією групою. Які методи мені потрібно буде перекрити в Fooкласі?

Підводячи підсумок: я хочу класифікувати Stuffоб'єкти за списками, згрупованими за Fooоб'єктами. StuffОб'єкти матимуть a, FooIDщоб пов’язати їх зі своєю категорією.

Відповіді:


151

За замовчуванням два важливі методи є GetHashCode()і Equals(). Важливо, що якщо дві речі рівні ( Equals()повертає істину), вони мають однаковий хеш-код. Наприклад, ви можете "повернути FooID;" як GetHashCode()якщо ви хочете це як відповідність. Ви також можете реалізувати IEquatable<Foo>, але це необов'язково:

class Foo : IEquatable<Foo> {
    public string Name { get; set;}
    public int FooID {get; set;}

    public override int GetHashCode() {
        return FooID;
    }
    public override bool Equals(object obj) {
        return Equals(obj as Foo);
    }
    public bool Equals(Foo obj) {
        return obj != null && obj.FooID == this.FooID;
    }
}

Нарешті, ще одна альтернатива - забезпечити IEqualityComparer<T>виконання того ж.


5
+1, і я не хочу викрадати цю тему, але я мав враження, що GetHashCode () повинен повернути FooId.GetHashCode (). Це не правильний зразок?
Кен Браунінг

8
@Ken - ну просто потрібно повернути Int, який надає необхідні функції. FooID, який буде робити так само добре, як і FooID.GetHashCode (). Як деталь реалізації, Int32.GetHashCode () - це "повернути це;". Для інших типів (рядок тощо) тоді так: .GetHashCode () було б дуже корисно.
Marc Gravell

2
Дякую! Я пішов з IEqualityComparer <T>, тому що тільки для Дікаріонаря мені потрібні були переосмислені методи.
Дана

1
Ви повинні знати, що продуктивність контейнерів на основі хештелів (словник <T>, словник, HashTable тощо) залежить від якості використовуваної хеш-функції. Якщо ви просто FooID як хеш-код, контейнери можуть працювати дуже погано.
Jørgen Fogh

2
@ JørgenFogh Я дуже це знаю; представлений приклад узгоджується із заявленим наміром. Існує чимало супутніх проблем, пов’язаних із незмінністю хешу; Ідентифікатори змінюються рідше, ніж імена, і, як правило, надійно унікальні та показники еквівалентності. Хоча нетривіальна тема.
Марк Гравелл

33

Оскільки ви хочете, FooIDщоб ідентифікатор для групи, ви повинні використовувати це як ключ у словнику замість об'єкта Foo:

Dictionary<int, List<Stuff>>

Якщо ви будете використовувати Fooоб'єкт в якості ключа, ви б просто реалізувати GetHashCodeі Equalsметод розглядати тільки FooIDвластивість. NameМайно буде просто мертвим вантажем, наскільки Dictionaryбув зацікавлений, так що ви б просто використовувати в Fooякості обгортки для int.

Тому краще використовувати FooIDзначення безпосередньо, і тоді вам не доведеться нічого реалізовувати, як Dictionaryуже підтримує, використовуючи intключ як ключ.

Редагувати:
Якщо ви хочете все-таки використовувати Fooклас як ключ, IEqualityComparer<Foo>це легко здійснити:

public class FooEqualityComparer : IEqualityComparer<Foo> {
   public int GetHashCode(Foo foo) { return foo.FooID.GetHashCode(); }
   public bool Equals(Foo foo1, Foo foo2) { return foo1.FooID == foo2.FooID; }
}

Використання:

Dictionary<Foo, List<Stuff>> dict = new Dictionary<Foo, List<Stuff>>(new FooEqualityComparer());

1
Більш правильно, int вже підтримує методи / інтерфейси, необхідні для його використання в якості ключа. Словник не має прямих знань про int чи будь-який інший тип.
Джим Мішель

Я думав про це, але з різних причин було чистіше і зручніше використовувати об’єкти як клавіші словника.
Дана

1
Добре, це виглядає лише так, що ви використовуєте об'єкт як ключ, оскільки ви дійсно використовуєте ідентифікатор як ключ.
Гуффа

8

Для Foo вам потрібно буде замінити object.GetHashCode () та object.Equals ()

Словник закликає GetHashCode () для обчислення хеш-відра для кожного значення та дорівнює, щоб порівняти, чи два Foo однакові.

Не забудьте обчислити хороші хеш-коди (уникайте багатьох рівних об'єктів Foo, які мають однаковий хеш-код), але переконайтесь, що два рівні Foos мають однаковий хеш-код. Ви можете почати з методу рівняння, а потім (в GetHashCode ()) або скористатися хеш-кодом кожного члена, який ви порівнюєте в рівних.

public class Foo { 
     public string A;
     public string B;

     override bool Equals(object other) {
          var otherFoo = other as Foo;
          if (otherFoo == null)
             return false;
          return A==otherFoo.A && B ==otherFoo.B;
     }

     override int GetHashCode() {
          return 17 * A.GetHashCode() + B.GetHashCode();
     }
}

2
Убік - але xor (^) робить поганий комбінатор хеш-кодів, оскільки це часто призводить до безлічі діагональних зіткнень (тобто {"foo", "bar"} vs {"bar", "foo"}. А краще вибір - множити та додавати кожен доданок - тобто 17 * a.GetHashCode () + B.GetHashCode ();
Марк Гравелл

2
Марк, я бачу, що ти маєш на увазі. Але як потрапити на чарівне число 17? Чи вигідно використовувати просте число як множник для комбінування хешей? Якщо так, то чому?
froh42

Чи можу я запропонувати повернути: (A + B). ціле переповнення.
Чарльз Бернс

(A + B) .GetHashCode () створює дуже поганий алгоритм хешування, оскільки різні набори (A, B) можуть призвести до одного і того ж хешу, якщо вони об'єднані в одну і ту ж рядок; "hellow" + "ned" - це те саме, що "ада" + "володіє" і призведе до того ж хешу.
kaesve

@kaesve як щодо (A + "" + B) .GetHashCode ()?
позачасовий

1

Що про Hashtableклас!

Hashtable oMyDic = new Hashtable();
Object oAnyKeyObject = null;
Object oAnyValueObject = null;
oMyDic.Add(oAnyKeyObject, oAnyValueObject);
foreach (DictionaryEntry de in oMyDic)
{
   // Do your job
}

Вище можна використовувати будь-який об’єкт (об’єкт класу) як загальний ключ словника :)


1

У мене була така ж проблема. Тепер я можу використовувати будь-який об’єкт, який я спробував, як ключ, завдяки переважаючим рівним та GetHashCode.

Ось клас, який я створив за допомогою методів використання всередині переопределень рівних (об’єкт obj) та GetHashCode (). Я вирішив використовувати дженерики та алгоритм хешування, який повинен мати можливість охопити більшість об’єктів. Будь ласка, дайте мені знати, якщо ви бачите щось, що не працює для деяких типів об'єкта, і у вас є спосіб його вдосконалити.

public class Equality<T>
{
    public int GetHashCode(T classInstance)
    {
        List<FieldInfo> fields = GetFields();

        unchecked
        {
            int hash = 17;

            foreach (FieldInfo field in fields)
            {
                hash = hash * 397 + field.GetValue(classInstance).GetHashCode();
            }
            return hash;
        }
    }

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

        return Equals(classInstance, (T)obj);
    }

    private bool Equals(T classInstance, T otherInstance)
    {
        List<FieldInfo> fields = GetFields();

        foreach (var field in fields)
        {
            if (!field.GetValue(classInstance).Equals(field.GetValue(otherInstance)))
            {
                return false;
            }
        }

        return true;
    }

    private List<FieldInfo> GetFields()
    {
        Type myType = typeof(T);

        List<FieldInfo> fields = myType.GetTypeInfo().DeclaredFields.ToList();
        return fields;
    }
}

Ось як він використовується в класі:

public override bool Equals(object obj)
    {
        return new Equality<ClassName>().Equals(this, obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return new Equality<ClassName>().GetHashCode(this);
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.