Использование поля объекта в качестве универсального ключа словаря

120

Если я хочу использовать объекты в качестве ключей для a Dictionary, какие методы мне нужно будет переопределить, чтобы они сравнивались определенным образом?

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

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

    // elided
} 

И я хочу создать:

Dictionary<Foo, List<Stuff>>

Я хочу, Fooчтобы одинаковые объекты FooIDсчитались одной и той же группой. Какие методы мне нужно будет переопределить в Fooклассе?

Подводя итог: я хочу разбить Stuffобъекты на списки, сгруппированные по Fooобъектам. Stuffобъекты будут иметь FooIDссылку на их категорию.

Dana
источник

Ответы:

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 () было бы очень полезно.
Марк Гравелл
2
Спасибо! Я выбрал IEqualityComparer <T>, так как только для Dicarionary мне понадобились методы переопределения.
Дана
1
Вы должны знать, что производительность контейнеров на основе хэш-таблиц (Dictionary <T>, Dictionary, 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());
Guffa
источник
1
Вернее, int уже поддерживает методы / интерфейсы, необходимые для его использования в качестве ключа. Словарь не имеет прямого знания о int или любом другом типе.
Джим Мишель,
Я думал об этом, но по ряду причин было чище и удобнее использовать объекты в качестве ключей словаря.
Дана
1
Ну, это только похоже на то, что вы используете объект как ключ, поскольку вы действительно используете только id как ключ.
Guffa
8

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

Словарь вызовет GetHashCode () для вычисления хеш-сегмента для каждого значения и Equals для сравнения идентичности двух Foo.

Убедитесь, что вы вычислили хорошие хэш-коды (избегайте множества одинаковых объектов Foo, имеющих одинаковый хэш-код), но убедитесь, что два равных Foo имеют одинаковый хеш-код. Возможно, вы захотите начать с метода Equals, а затем (в GetHashCode ()) xor хэш-код каждого члена, который вы сравниваете в Equals.

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();
     }
}
froh42
источник
2
В сторону - но xor (^) является плохим комбинатором для хэш-кодов, поскольку он часто приводит к множеству диагональных коллизий (например, {"foo", "bar"} vs {"bar", "foo"}. Лучше выбор - умножить и сложить каждый член - например, 17 * a.GetHashCode () + B.GetHashCode ();
Marc Gravell
2
Марк, я понимаю, о чем ты. Но как получить волшебное число 17? Выгодно ли использовать простое число в качестве умножителя для объединения хешей? Если да, то почему?
froh42
Могу я предложить вернуть: (A + B) .GetHashCode (), а не: 17 * A.GetHashCode () + B.GetHashCode () Это будет: 1) с меньшей вероятностью столкновения и 2) гарантией отсутствия целочисленное переполнение.
Чарльз Бернс
(A + B) .GetHashCode () создает очень плохой алгоритм хеширования, поскольку разные наборы (A, B) могут привести к одному и тому же хешу, если они объединены в одну строку; «hellow» + «ned» - это то же самое, что «hell» + «own», и результатом будет тот же хэш.
kaesve
@kaesve, а как насчет (A + "" + B) .GetHashCode ()?
Timeless
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

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

Вот класс, который я создал с методами для использования внутри переопределений Equals (объект 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);
        }
    }
kb4000
источник