Почему 2 экземпляра делегата возвращают один и тот же хэш-код?

Возьмите следующее:

  var x =  new Action(() => { Console.Write("") ; });
  var y = new Action(() => { });
  var a = x.GetHashCode();
  var b = y.GetHashCode();
  Console.WriteLine(a == b);
  Console.WriteLine(x == y);

Это напечатает:

True
False

Почему хэш-код такой же?

Это немного удивительно, и использование делегатов в Dictionary будет таким же медленным, как List (он же O(n) для поиска).

Обновление:

Вопрос в том, почему. IOW, кто принял такое (глупое) решение?

Лучшей реализацией хэш-кода было бы:

return Method ^ Target == null ? 0 : Target.GetHashcode();
// where Method is IntPtr

person leppie    schedule 08.07.2011    source источник
comment
Вы пытались поместить некоторый код и во второй делегат, и только после проверки хэш-кода?   -  person Tigran    schedule 08.07.2011
comment
Не знаю точно, ПОЧЕМУ это происходит, но в качестве идеи вы можете реализовать свой собственный механизм хеширования или обернуть эти действия в класс и переопределить его GetHashCode в соответствии с вашими потребностями.   -  person Can Poyrazoğlu    schedule 08.07.2011
comment
Просто для справки, контракт для Delegate.Equals: Определяет, относятся ли указанный объект и текущий делегат к одному и тому же типу и используют одни и те же цели, методы и список вызовов.   -  person CodesInChaos    schedule 08.07.2011
comment
@leppie, хотя я должен признаться, что у меня никогда не было сценария, в котором я хотел бы использовать делегата в качестве ключа в словаре. Как значение, наверняка - просто не ключ.   -  person Marc Gravell    schedule 08.07.2011
comment
Ваша лучшая реализация имеет ту же ошибку, что и я: вы вызываете переопределенное Target.GetHashCode вместо использования ссылочного равенства.   -  person CodesInChaos    schedule 08.07.2011
comment
@EricLippert: Какой-то комментарий? Извините за спам :)   -  person leppie    schedule 10.07.2011
comment
В настоящее время я создаю простой планировщик задач, который принимает Action в качестве входных данных. Попытка выяснить способ УНИКАЛЬНОЙ идентификации действий. пока не нашел :(   -  person Alex from Jitbit    schedule 10.11.2017


Ответы (4)


Легкий! Так как вот реализация GetHashCode (находится в базовом классе Delegate):

public override int GetHashCode()
{
    return base.GetType().GetHashCode();
}

(находится в базовом классе MulticastDelegate, который будет вызывать выше):

public sealed override int GetHashCode()
{
    if (this.IsUnmanagedFunctionPtr())
    {
        return ValueType.GetHashCodeOfPtr(base._methodPtr);
    }
    object[] objArray = this._invocationList as object[];
    if (objArray == null)
    {
        return base.GetHashCode();
    }
    int num = 0;
    for (int i = 0; i < ((int) this._invocationCount); i++)
    {
        num = (num * 0x21) + objArray[i].GetHashCode();
    }
    return num;
}

Используя такие инструменты, как Reflector, мы можем увидеть код, и кажется, что реализация по умолчанию такая же странная, как мы видим выше.

Значение типа здесь будет Action. Следовательно, приведенный выше результат правильный.

ОБНОВИТЬ

person Aliostad    schedule 08.07.2011
comment
Честно говоря, return 42; был бы примерно столь же полезен, как и реализация Delegate (хотя MulticastDelegate выглядит лучше). - person Marc Gravell; 08.07.2011
comment
Однако GetHashCode, в свою очередь, переопределяется MulticastDelegate, который суммирует хэш-коды всех подписанных делегатов (если это на самом деле многоадресный делегат). Сюжет закручивается... - person thecoop; 08.07.2011
comment
@Marc: Object.GetType() выполняет динамический поиск фактического типа объекта во время выполнения, поэтому это не то же самое, что typeof(object).GetHashCode(). На самом деле он возвращает разные значения для каждого типа делегата (но одно и то же значение для всех экземпляров этого типа). - person thecoop; 08.07.2011
comment
@Marc: я только что удалил свой комментарий; Реализация MulticastDelegate так же сломана, потому что комбинированный хэш-код будет генерироваться путем вызова хитроумной базовой реализации для каждой цели. Вы получите разные хэш-коды, если у вас будет разное количество целей, но если у вас одинаковое количество целей, вы получите один и тот же хэш-код! - person LukeH; 08.07.2011
comment
@thecoop: А как часто вы подписываете несколько методов на делегата в ситуации отсутствия событий? Я, наверное, могу сосчитать количество раз на половине руки :) - person leppie; 08.07.2011
comment
@thecoop - да, вычистил это - person Marc Gravell; 08.07.2011

Моя первая попытка лучшей реализации:

public class DelegateEqualityComparer:IEqualityComparer<Delegate>
{
    public bool Equals(Delegate del1,Delegate del2)
    {
        return (del1 != null) && del1.Equals(del2);
    }

    public int GetHashCode(Delegate obj)
    {
            if(obj==null)
                return 0;
            int result = obj.Method.GetHashCode() ^ obj.GetType().GetHashCode();
            if(obj.Target != null)
                result ^= RuntimeHelpers.GetHashCode(obj);
            return result;
    }
}

Качество этого должно быть хорошим для одноадресных делегатов, но не так много для многоадресных делегатов (если я правильно помню, цель/метод возвращает значения последнего делегата элемента).

Но я не совсем уверен, что он выполняет контракт во всех крайних случаях.

Хм, похоже, качество требует ссылочного равенства целей.

person CodesInChaos    schedule 08.07.2011
comment
@adrianm: Ваш комментарий не имеет смысла. - person leppie; 08.07.2011
comment
Хм, интересная мысль @adrianm. Я думаю, что спецификация equals может означать ссылочное равенство вместо .Equals() - person CodesInChaos; 08.07.2011
comment
Изменил его, чтобы использовать ссылочное равенство целей. - person CodesInChaos; 08.07.2011
comment
@leppie: если что-то внутри цели изменится, хэш-код делегата также изменится, то есть вы нарушите контракт ключей словаря. Я не могу точно сказать, почему, но я думаю, что странная реализация хэш-кода как-то связана с определением равенства и неизменяемыми списками вызовов. - person adrianm; 08.07.2011
comment
@adrianm: я понимаю, что вы имеете в виду, но никогда не следует использовать такие ключи! - person leppie; 08.07.2011

Это похоже на некоторые случаи, упомянутые в этой теме, возможно, это даст вам некоторые указания на это поведение. иначе, вы могли бы войти туда :-)

Какой самый странный угловой случай вы видели в C# или .NET?

С уважением, GJ

person gjvdkamp    schedule 08.07.2011
comment
какой именно пост? это не связано с вопросом, если вы это имеете в виду - person Marc Gravell; 08.07.2011

Из MSDN:

Реализация GetHashCode по умолчанию не гарантирует уникальности или согласованности; поэтому его нельзя использовать в качестве уникального идентификатора объекта для целей хеширования. Производные классы должны переопределять GetHashCode реализацией, которая возвращает уникальный хэш-код. Для достижения наилучших результатов хэш-код должен основываться на значении поля или свойства экземпляра, а не на статическом поле или свойстве.

Поэтому, если вы не перезаписали метод GetHashCode, он может вернуть то же самое. Я подозреваю, что это потому, что он генерирует его из определения, а не из экземпляра.

person Schroedingers Cat    schedule 08.07.2011
comment
То, что контракт позволяет это, очевидно. Вопрос в том, почему реализация такая плохая. - person CodesInChaos; 08.07.2011
comment
Я хотел сказать, что MS уклоняется от этого, говоря, что вам нужно переопределить реализацию по умолчанию, чтобы получить уникальные значения. Это означает, что реализация по умолчанию бесполезна. Как указал Алиостад выше, и я пытался сказать, значение по умолчанию основано на типе, а не на экземпляре. Наверное, потому, что MS нас ненавидит. - person Schroedingers Cat; 08.07.2011
comment
Теперь, если бы мы только могли переопределить хэш-код в C#. Я знаю, что в CLR (через IL) можно создавать собственные (и очень медленные) делегаты. - person leppie; 08.07.2011
comment
@leppie: Если вас беспокоит производительность в хеш-таблицах и т. д., вы можете просто передать IEqualityComparer<T>, который делает правильные вещи, в ваши конструкторы Dictionary<K,V>/HashSet<T> и т. д., или настроить пользовательские DelegateDictionary<K,V>/DelegateHashSet<T>, которые делают это автоматически. - person LukeH; 08.07.2011
comment
@leppie: По общему признанию, это не идеально, и вам действительно не нужно делать это в первую очередь, но, вероятно, это адекватный обходной путь. - person LukeH; 08.07.2011
comment
@LukeH: я согласен и сделаю это, если когда-нибудь понадобится :) - person leppie; 08.07.2011
comment
@LukeH, чтобы сделать это, нам сначала нужно выяснить, что правильно. И это не так просто. Так что было бы очень хорошо, если бы MS предоставила хорошую реализацию по умолчанию. - person CodesInChaos; 08.07.2011