V3229. The 'GetHashCode' method may return different hash codes for equal objects. It uses an object reference to generate a hash for a variable. Check the implementation of the 'Equals' method.

Анализатор обнаружил, что результат GetHashCode зависит от хэш-кода ссылки на некоторый объект. При этом результат Equals зависит от логики сравнения этого объекта, отличной от сравнения двух ссылок. Это может привести к нарушению контракта: два одинаковых объекта согласно методу Equals должны также иметь одинаковые хэш-коды.

Нарушение контракта методов GetHashCode и Equals может привести к некорректной работе некоторых функций и структур данных:

Рассмотрим пример:

public HashSet<Value> Values { get; private set; }  = new();
....
public override bool Equals(object obj)
{
  if (obj is not CustomObject other)
    return false;

  return Values.SequenceEqual(other.Values);
}

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

В качестве возвращаемого значения GetHashCode некоторого класса используется хэш-код от ссылки на коллекцию Values. При этом в методе Equals происходит поэлементное сравнение с этой коллекцией с помощью SequenceEqual.

Способ N1. Упростить сравнение объектов:

public HashSet<Value> Values { get; private set; } = new();
....
public override bool Equals(object obj)
{
  if (obj is not CustomObject other)
    return false;

  return Values.Equals(other.Values);
}

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

Способ N2. Алгоритм в GetHashCode также должен учитывать хэши объектов коллекции:

public readonly HashSet<Value> Values { get; private set; } = new();
....
public override bool Equals(object obj)
{
  if (obj is not CustomObject other)
    return false;

  return Values.SequenceEqual(other.Values);
}

public override int GetHashCode()
{
  return Values.Aggregate(0,
           (hash, val) =>   (hash * 397) 
                          ^ (val?.GetHashCode() ?? 0));
}

Вызов Aggregate формирует общий хэш-код элементов коллекции путём их перебора и обновления значения hash на каждой итерации согласно функции, переданной вторым аргументом.

Примечание. Необходимо также учитывать, что к методу GetHashCode предъявляются требования по скорости работы, а способ N2 может нарушить их.