Distinct и Co - грабли в поле
От: btn1  
Дата: 26.03.15 15:11
Оценка: -6 :))) :)))
Предыстория вопроса: Есть у нас IEnumerable<T>.Distinct; Метод, конечно, хороший, но был спроектирован на "отъ***ись" (кол бы потесать на башке .NET team leader). Почему? Да потому что при использовании сего метода, кастомный "сравниватель на эквивалентность" не может быть лямбдой! (хотя казалось бы, зачем вообще вводить лямбды, если вы их толком не даёте использовать?!!) Поясню примером:

var lst = new List<MyClass>(); // почти в каждой программе есть списки "непойми чего", элементы которых НЕ МОГУТ сравниваться "в лоб"
var uniq = lst.Distinct((a, b) => { return a.SomeField == b.SomeField; }); // это КАК НАДО было проектировать Distinct, чтобы он был реально полезен


Ну что же, если гора идиотов не понимает Магомета, Магомет делает всё сам! Итак, пишем класс-помощник (берите на заметку!):

public class EqualityCompareWrapper<T> :  EqualityComparer<T>
{
        private Func<T, T, Boolean> _comparer;

        public EqualityCompareWrapper(Func<T, T, Boolean> comparer)
        {
            _comparer = comparer;
        }

        public override bool Equals(T x, T y)
        {
            return this._comparer(x, y);
        }

        public override int GetHashCode(T obj)
        {
            return obj.GetHashCode();
        }
}


Это "обёртка" для лямбд, торчащая наружу как IEqualityComparer<T>. А далее всё просто — пишем "тёзку" Distinct, принимающего человеческие лямбды!

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source, Func<T, T, Boolean> comparer)
{
    return source.Distinct<T>(new EqualityCompareWrapper<T>(comparer));
}


Вуаля! Возвращаемся к самому верхнему примеру и видим, что он работает! Но как вы понимаете, в мелкософте сидят писатели, а не использователи, поэтому граблями прилетело в самом неожиданном месте — на тривиальных строках!
Итак, опять у нас есть коллекция — в этот раз строк, и мы хотим получить только уникальные строки, причём без учёта регистра. Думаю, последний мешочник с павелецкого вокзала уже догадался как:

var lst = new List<string>(new[] { "ZZzz", "aaAA", "zzzz", "aaaa" });
var uq = lst.Distinct((a, b) => { return a.Equals(b, StringComparison.InvariantCultureIgnoreCase); });
Console.WriteLine("RES: " + string.Join(", ", uq) );


Вы ожидаете здесь что-то типа "RES: ZZzz, aaAA"? А вот на тебе граблёй куда не ждал! Результат: "RES: ZZzz, aaAA, zzzz, aaaa"!!!!
После получаса ковыряния в недрах дистинктов нашёлся и виновник — оказывается, метод Distinct для проверки уникальности элемента юзает внутренний класс Set<T>, который имеет метод Find, который в своих индусячих недрах проверяет совпадение ... учитывая хэш!! Вот код заразы:

if ((this.slots[i].hashCode == hashCode) && this.comparer.Equals(this.slots[i].value, value))


Если бы не было проверки до &&, наша лямбда прекрасно бы отработала (а накой тогда мы передаёт тебе comparer-то?!), но вот этот hashCode для строк разного регистра РАЗНЫЙ.
Отсюда и все грабли. Зачем делать подобное сравнение хэшей — ума не приложу! Возможно, кто-то умный, но слишком погруженный в недра дотнета, решил, что это классная идея, но абсолютно забыл, что это _МЫ_ решаем (через comparer) кто имеет право быть равным!
Вот что значит сначала переименовывать Жабоклассы под видом сишарповых, и только потом думать о грамотном проектировании коллекций — ФУНДАМЕНТА всего дотнета. ((
Re: Distinct и Co - грабли в поле
От: Qulac Россия  
Дата: 26.03.15 15:24
Оценка: +3
Вообще так и было задумано. Компаратор для равных объектов должен возвращать одинаковый хеш. У вас в итоге не так получается.
Программа – это мысли спрессованные в код
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 26.03.15 15:41
Оценка:
Здравствуйте, Qulac, Вы писали:

Q>Вообще так и было задумано. Компаратор для равных объектов должен возвращать одинаковый хеш.


Какого чёрта тогда нужно передавать отдельный сравниватель, если можно обойтись одним хэшем??
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 26.03.15 15:46
Оценка: :)
Здравствуйте, Qulac, Вы писали:

Небольшой оффтопик: Программа – это мысли РАЗМАЗАННЫЕ в код. Ибо мысль более компактна.
Re: Distinct и Co - грабли в поле
От: hardcase Пират http://nemerle.org
Дата: 26.03.15 15:46
Оценка: +1
Здравствуйте, btn1, Вы писали:

B>
B>var lst = new List<string>(new[] { "ZZzz", "aaAA", "zzzz", "aaaa" });
B>var uq = lst.Distinct((a, b) => { return a.Equals(b, StringComparison.InvariantCultureIgnoreCase); });
B>Console.WriteLine("RES: " + string.Join(", ", uq) );
B>


Для строк необходимо использовать не твою лямбду, а нормальный StringComparer.InvariantCultureIgnoreCase сравниватель, он будет возвращать равные хэшкоды для равных с его точки зрения строк.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[3]: Distinct и Co - грабли в поле
От: hi_octane Беларусь  
Дата: 26.03.15 17:49
Оценка: +1
B>Какого чёрта тогда нужно передавать отдельный сравниватель, если можно обойтись одним хэшем??

Одним хэшем обойтись нельзя. Хэш не гарантирует что объекты равны. Он лишь "быстро" говорит что они настолько "похожи", что "медленное" сравнение имеет смысл.
Re: Distinct и Co - грабли в поле
От: _temp  
Дата: 26.03.15 18:06
Оценка: 6 (1)
Здравствуйте, btn1, Вы писали:

B>Предыстория вопроса: Есть у нас IEnumerable<T>.Distinct; Метод, конечно, хороший, но был спроектирован на "отъ***ись" (кол бы потесать на башке .NET team leader). Почему? Да потому что при использовании сего метода, кастомный "сравниватель на эквивалентность" не может быть лямбдой! (хотя казалось бы, зачем вообще вводить лямбды, если вы их толком не даёте использовать?!!) Поясню примером:

B>var lst = new List<MyClass>(); // почти в каждой программе есть списки "непойми чего", элементы которых НЕ МОГУТ сравниваться "в лоб"
B>var uniq = lst.Distinct((a, b) => { return a.SomeField == b.SomeField; }); // это КАК НАДО было проектировать Distinct, чтобы он был реально полезен


Передо мной такие задачи встают регулярно, я делаю так:
private static readonly EqualityComparer<MyClass> MyClassBySomeFieldEqualityComparer = new ComparerBuilder<MyClass>()
  .AddEquality(item => item.SomeField)
  .ToEqualityComparer();
…
var items = new List<MyClass>();
var unique = items.Distinct(MyClassBySomeFieldEqualityComparer);

Если нужно сравнивать несколько полей из MyClass, и/или надо не только сравнивать на равенство но и на больше-меньше/сортировать, то так:
private static readonly ComparerBuilder<MyClass> MyClassCustomComparerBuilder = new ComparerBuilder<MyClass>()
  .Add(item => item.SomeField)
  // Если надо без учёта регистра
  .Add(item => item.SomeTextField, StringComparer.OrdinalIgnoreCase);
  // Так же, можно удобно и просто справнивать не только "линейные" поля, 
  // но и произвольные колекции/словари/мульти-словари и т.п.; с учётом порядка элементов или без него

private static readonly EqualityComparer<MyClass> MyClassCustomEqualityComparer = MyClassCustomComparerBuilder.ToEqualityComparer();
private static readonly Comparer<MyClass> MyClassCustomComparer = MyClassCustomComparerBuilder.ToComparer();

…
var items = new List<MyClass>();
items.Sort(MyClassCustomComparer);
var unique = items.Distinct(MyClassCustomEqualityComparer);

Добавление нового поля таким образом ни разу не является проблемой и дублирование кода исключено. О чёрной магии хеш-кодов можно забыть как страшный сон и и не знать о ней ещё сто лет.

Класс ComparerBuilder<> есть тут в Исходниках и небольшой докой
Автор: _temp
Дата: 22.03.15
, более свежий на кодеплексе. А на гитхабе более удобная версия с возможностью отладки и перехвата в пользовательском коде операций сравнения и другими улучшениями.
Отредактировано 26.03.2015 18:09 _temp (Code fixed) . Предыдущая версия .
Re: Distinct и Co - грабли в поле
От: Jack128  
Дата: 27.03.15 06:59
Оценка:
Здравствуйте, btn1, Вы писали:

Если на производительность совсем наплевать, то просто в своем Wrapper'e в GetHashCode всегда 0 возвращай для любого объекта
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 27.03.15 13:00
Оценка: -1
Здравствуйте, hardcase, Вы писали:

H>Для строк необходимо использовать не твою лямбду, а нормальный StringComparer.InvariantCultureIgnoreCase


В данном узком и простом примере — да (спасибо за наводку). А если я сравниваю людей и ФИО мне нужны case insensitive, но при этом возраст может "чуть отличаться"? (лет на 5) Лямбду 100% придётся писать и тут опять вылезает злополучный хэш, очевидно разный для "почти одинаковых" объектов.
НЕ НУЖЕН этот хэш — это оптимизация по скорости, тупо ставящая бомбу под целый класс задач.
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 27.03.15 13:05
Оценка:
Здравствуйте, Jack128, Вы писали:

J>Если на производительность совсем наплевать, то просто в своем Wrapper'e в GetHashCode всегда 0 возвращай для любого объекта


+1. Хороший workaround, для "самопальных лямбд" вполне подходит. Тем более, что начисто устраняет проблему.
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 27.03.15 13:07
Оценка:
Здравствуйте, Jack128, Вы писали:

J>Если на производительность совсем наплевать...


Кстати, она тут не причём: мы всего лишь делаем пустое сравнение 0 == 0, ведь реальное сравнение всё равно должно выполниться в лямбде.
Re[2]: Distinct и Co - грабли в поле
От: btn1  
Дата: 27.03.15 13:14
Оценка: -1
Здравствуйте, _temp, Вы писали:

_>Передо мной такие задачи встают регулярно, я делаю так:

_>
_>private static readonly EqualityComparer<MyClass> MyClassBySomeFieldEqualityComparer = new ComparerBuilder<MyClass>()
_>  .AddEquality(item => item.SomeField)
_>  .ToEqualityComparer();
_>…
_>var items = new List<MyClass>();
_>var unique = items.Distinct(MyClassBySomeFieldEqualityComparer);

_темп, спасибо за помощь. Но мне это решение кажется таким же громоздким - три вспомогательных строки + четвёртая рабочая, да ещё всё это заточенное исключительно на класс MyClass. Мне хотелось решения в одну строку - не такая это архиважная задача, чтобы решать её кучей кода. Фактически всё решилось возвращением нуля:

[c#]
public class EqualityCompareWrapper<T> :  EqualityComparer<T>
{
    private Func<T, T, Boolean> _comparer;

    public EqualityCompareWrapper(Func<T, T, Boolean> comparer)
    {
        _comparer = comparer;
    }

    public override bool Equals(T x, T y)
    {
        return this._comparer(x, y);
    }

    public override int GetHashCode(T obj)
    {
        return 0;//ATTN: грабли! Оденьте пробковый шлем.
    }
}


Re[3]: Distinct и Co - грабли в поле
От: Sinix  
Дата: 27.03.15 13:28
Оценка:
Здравствуйте, btn1, Вы писали:

J>>Если на производительность совсем наплевать...

B>Кстати, она тут не причём: мы всего лишь делаем пустое сравнение 0 == 0, ведь реальное сравнение всё равно должно выполниться в лямбде.

Ну да, O(n2) vs O(n). Фигня же.
Re[3]: Distinct и Co - грабли в поле
От: Sinix  
Дата: 27.03.15 13:31
Оценка: +2
Здравствуйте, btn1, Вы писали:

B> А если я сравниваю людей и ФИО мне нужны case insensitive, но при этом возраст может "чуть отличаться"? (лет на 5)


По определению fail. Сравнение должно быть транзитивным, у вас:
5 лет == 10 лет
10 лет == 15 лет
5 лет != 15 лет.
Re: Distinct и Co - грабли в поле
От: Serginio1 СССР https://habrahabr.ru/users/serginio1/topics/
Дата: 27.03.15 13:46
Оценка:
Здравствуйте, btn1, Вы писали:


B>Вы ожидаете здесь что-то типа "RES: ZZzz, aaAA"? А вот на тебе граблёй куда не ждал! Результат: "RES: ZZzz, aaAA, zzzz, aaaa"!!!!

B>После получаса ковыряния в недрах дистинктов нашёлся и виновник — оказывается, метод Distinct для проверки уникальности элемента юзает внутренний класс Set<T>, который имеет метод Find, который в своих индусячих недрах проверяет совпадение ... учитывая хэш!! Вот код заразы:

B>
B>if ((this.slots[i].hashCode == hashCode) && this.comparer.Equals(this.slots[i].value, value))
B>


B>Если бы не было проверки до &&, наша лямбда прекрасно бы отработала (а накой тогда мы передаёт тебе comparer-то?!), но вот этот hashCode для строк разного регистра РАЗНЫЙ.

B>Отсюда и все грабли. Зачем делать подобное сравнение хэшей — ума не приложу! Возможно, кто-то умный, но слишком погруженный в недра дотнета, решил, что это классная идея, но абсолютно забыл, что это _МЫ_ решаем (через comparer) кто имеет право быть равным!
B>Вот что значит сначала переименовывать Жабоклассы под видом сишарповых, и только потом думать о грамотном проектировании коллекций — ФУНДАМЕНТА всего дотнета. ((

Есть несколько вариантов поиска дублей 1 в Хэш таблице 2 двоичным поиском по отсортированному списку, ну и по варианту всем известным пузырьком, который ты и предлагаешь.
Индусы выбрали первый вариант.

https://ru.wikipedia.org/wiki/%D0%9E%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F_%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F_(%D0%A1%D0%A3%D0%91%D0%94)
и солнце б утром не вставало, когда бы не было меня
Re: Distinct и Co - грабли в поле
От: koandrew Канада http://thingselectronic.blogspot.ca/
Дата: 31.03.15 17:41
Оценка: +1 :)
Здравствуйте, btn1, Вы писали:

Старая история от старого btn1: не прочитав спеку до конца (на тему того, что такое компаратор и какими алгебраическими свойствами он должен обладать) начинает обвинять всех и вся в том, что на поверку оказывается результатом собственной неспособности осилить документацию.
Обычно в таком случае советуют подрасти и поднабраться опыта, а вот в твоём случае уже и не знаю, чего и советовать — наверное пчёл начать лучше разводить
[КУ] оккупировала армия.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.