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


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.