Реализация правильного GetHashCode

У меня следующий класс

public class ResourceInfo
{
    public string Id { get; set; }
    public string Url { get; set; }
}

который содержит информацию о каком-то ресурсе. Теперь мне нужна возможность проверить, равны ли два таких ресурса по следующему сценарию (я реализовал интерфейс IEquatable)

public class ResourceInfo : IEquatable<ResourceInfo>
{
    public string Id { get; set; }
    public string Url { get; set; }

    public bool Equals(ResourceInfo other)
    {
        if (other == null)
            return false;

        // Try to match by Id
        if (!string.IsNullOrEmpty(Id) && !string.IsNullOrEmpty(other.Id))
        {
            return string.Equals(Id, other.Id, StringComparison.InvariantCultureIgnoreCase); 
        }

        // Match by Url if can`t match by Id
        return string.Equals(Url, other.Url, StringComparison.InvariantCultureIgnoreCase);
    }
}

Использование: oneResource.Equals(otherResource). И все просто отлично. Но прошло какое-то время, и теперь мне нужно использовать такое сравнение eqaulity в каком-то linq-запросе.

В результате мне нужно реализовать отдельный компаратор Equality, который выглядит так:

class ResourceInfoEqualityComparer : IEqualityComparer<ResourceInfo>
{
    public bool Equals(ResourceInfo x, ResourceInfo y)
    {
        if (x == null || y == null)
            return object.Equals(x, y);

        return x.Equals(y);
    }

    public int GetHashCode(ResourceInfo obj)
    {
        if (obj == null)
            return 0;

        return obj.GetHashCode();
    }
}

Кажется, все в порядке: он создает некоторую логику проверки и использует собственную логику сравнения равенства. Но затем мне нужно реализовать метод GetHashCode в классе ResourceInfo, и это то место, где у меня есть некоторые проблемы.

Не знаю, как это правильно сделать, не меняя сам класс.

На первый взгляд, следующий пример может работать

public override int GetHashCode()
{
    // Try to get hashcode from Id
    if(!string.IsNullOrEmpty(Id))
        return Id.GetHashCode();
    // Try to get hashcode from url
    if(!string.IsNullOrEmpty(Url))
        return Url.GetHashCode();

    // Return zero
    return 0;
}

Но это не очень хорошая реализация.

GetHashCode должен соответствовать методу Equals: если два объекта равны, то у них должен быть одинаковый хэш-код, верно? Но мой метод Equals использует два объекта для их сравнения. Вот пример использования, в котором вы можете увидеть саму проблему:

var resInfo1 = new ResourceInfo()
{
    Id = null,
    Url = "http://res.com/id1"
};
var resInfo2 = new ResourceInfo()
{
    Id = "id1",
    Url = "http://res.com/id1"
};

Итак, что произойдет, когда мы вызовем метод Equals: очевидно, что они будут равны, потому что метод Equals попытается сопоставить их по идентификатору и потерпит неудачу, затем он попытается сопоставить их по URL-адресу, и здесь у нас есть те же значения. Как предполагалось.

resInfo1.Equals(resInfo1 ) -> true

Но тогда, если они равны, у них должны быть одинаковые хэш-коды:

var hash1 = resInfo.GetHashCode(); // -263327347
var hash2 = resInfo.GetHashCode(); // 1511443452

hash1.GetHashCode() == hash2.GetHashCode() -> false

Короче говоря, проблема в том, что метод Equals решает, какое поле использовать для сравнения на равенство, глядя на два разных объекта, в то время как метод GetHashCode имеет доступ только к одному объекту.

Есть ли способ реализовать это правильно или мне просто нужно изменить свой класс, чтобы избежать таких ситуаций?

Большое спасибо.


person steavy    schedule 14.07.2014    source источник


Ответы (1)


Ваш подход к равенству в корне нарушает спецификации в _1 _ .

В частности, примите во внимание:

var x = new ResourceInfo { Id = null, Uri = "a" };
var y = new ResourceInfo { Id = "yz", Uri = "a" };
var z = new ResourceInfo { Id = "yz", Uri = "b" };

Здесь x.Equals(y) будет истинным, а y.Equals(z) будет истинным, но x.Equals(z) будет ложным. Это прямо запрещено в документации:

  • Если (x.Equals(y) && y.Equals(z)) вернет истину, то x.Equals(z) вернет истину.

По сути, вам нужно будет переделать дизайн.

person Jon Skeet    schedule 14.07.2014
comment
@Chris: Это определенно похоже, хотя и не совсем, поскольку здесь идентификаторы учитываются только в том случае, если они не равны нулю. И да, определенное чувство дежавю ... но мне часто бывает сложно найти хороших обманов ... - person Jon Skeet; 14.07.2014
comment
Да, я искал дураков, но это тоже было трудно. О хэш-кодах много, но бывает так много разных ситуаций. Может быть, нам нужно задать исчерпывающий вопрос, чтобы сослаться на ... - person Chris; 14.07.2014