Думаю, что каждый программист рано или поздно сталкивается с кодом, который работает «не так, как ты от него ожидаешь». Именно это и подтолкнуло меня к написанию следующей статьи, в которой я пытаюсь понять, почему Except в Linq работает так, как написан, а не так, как я хочу. Что, по вашему мнению, должен вывести следующий код: Я вот предположил, что ничего, потому что Except должен вычитать множество (IEnumerable) правого аргумента из множества (IEnumerable) левого аргумента. Однако, вопреки моим ожиданиям я получил: Вне всякого сомнения — это не похоже на пустое множество. Давайте попробуем разобраться в том, почему так получается (результат в .NET 5 и в .NET 6 — эквивалентен). Чтобы понять, почему так происходит, и что можно с этим сделать обратимся к документации метода Except. Там действительно написано, что этот метод «Находит разность множеств, представленных двумя последовательностями» и имеет две перегрузки: Except<TSource>(IEnumerable<TSource>,   IEnumerable<TSource>) Находит   разность множеств, представленных двумя последовательностями, используя для   сравнения значений компаратор проверки на равенство по умолчанию. Except<TSource>(IEnumerable<TSource>,   IEnumerable<TSource>, IEqualityComparer<TSource>) Находит   разность множеств, представленных двумя последовательностями, используя для   сравнения значений указанный компаратор IEqualityComparer<T>. Обратите внимание на заявление о том, что для сравнения используется компаратор по умолчанию. Чтобы понять, почему наш код сработал именно так, как сработал, нам предстоит разобраться с поведением компаратора по умолчанию. Для этого я предлагаю проследовать на https://github.com/dotnet/runtime/ и проанализировать работу метода Except. Наша точка входа: Метод проверяет, что получил два объекта с ненулевым указателем и передает аргументы в ExceptIterator. Строго говоря, можно было бы использовать и метод перегрузку, так как он работает буквально также: Почему перегрузка, а не параметр по умолчанию? Силами сообщества было вынесено предположение, что дело в этом и этом. Собственно код ExceptIterator: Из элементов второго аргумента создается множество. Элементы первого аргумента, которые удалось добавить в множество, возвращаются в качестве итератора. Конструктор множества принимает интерфейс компаратора, который используется для сравнения элементов множества:   Конкретно в нашем случае компаратор равен null, поэтому проваливаемся в свойство Default обобщенного класса EqualityComparer.   Тут, на мой скромный взгляд, все очевидно:   Комментарий гласит, что с целью минимизации накладных расходов на создание универсального экземпляра для каждого типа код обобщенных классов уменьшают насколько это возможно за счет переноса его логики в необобщенный класс (в нашем случае это ComparerHelpers). Давайте же посмотрим на то, как происходит процесс создания компаратора по умолчанию: Пойдем по порядку:  1.       Если тип аргумента byte, то возвращается компаратор специально для этого типа (ByteEqualityComparer)  2.       Если это строка, то возвращается GenericEqualityComparer<string>(); 3.       Если тип реализует IEquatable, то на основе GenericEqualityComparer<string> возвращается GenericEqualityComparer для типа аргумента (даже не спрашивайте); 4.       Если аргумент является универсальным типом (обобщением) и если этот универсальный тип Nullable<>,  то на основе NullableEqualityComparer<int>  создается NullableEqualityComparer для типа аргумента; 5.       Если аргумент – перечисление, то на основе EnumEqualityComparer<> создается EnumEqualityComparer; 6.       Во всех остальных случаях на основе ObjectEqualityComparer<object> создается ObjectEqualityComparer. С помощью такого нехитрого кода (хотел было переписать через string builder, но, думаю, тут можно забить :D) попробуем понять, какой же из перечисленных случаев – наш: Что и следовало ожидать:  Это значит, что теперь наш путь лежит в ObjectEqualityComparer. Вот, собственно, и он: ObjectEqualityComparer определяет метод Equals для двух объектов следующим образом:   ·         Объекты равны, если они оба null (что логично); ·         Объекты не равны, если только один из них null; ·         Если оба объекта не null, то их эквивалентность определяется методом Equals «левого» аргумента. Если обратиться к документации, то можно увидеть, что у нашего класса FileInfo действительно есть метод Equals с пометкой «Унаследовано от Object». Что же, туда и лежит наш путь! Там в секции «комментарии» мы можем узнать, что: Если текущий экземпляр является ссылочным типом, Equals(Object) метод проверяет равенство ссылок, а вызов Equals(Object) метода эквивалентен вызову ReferenceEquals метода. Равенство ссылок означает, что сравниваемые объектные переменные ссылаются на один и тот же объект. На этом, казалось, можно было бы завершить наше путешествие, но давайте на последок придумаем, как заставить Except перестать показывать файлы в директории с моими документами. Вариант из категории «пока так, потом пофикшу»: Мы, по сути, вызываем Except для двух IEnumerate<string>, а потом из результата снова собираем IEnumerate<FileInfo>.   В свежем .net6 еще можно воспользоваться ExceptBy: Этот код уже выглядит приятнее и даже избавляет нас от необходимости разбирать и снова собирать изначальный массив, но можно сделать это иначе. Следующей идеей, посетившей мою голову, было унаследовать FileInfo и переопределить Equals, но, к сожалению, FileInfo является запечатанным (sealed) классом, а это значит, что он не может быть унаследован, так что этот путь нам отрезан. Подойдем к вопросу с другой стороны. Вспомним, что Equals имеет перегрузку, принимающую вторым аргументом IEqualityComparer, что позволяет нам создать что-то вроде такого решения:  Это решение хорошо подходит в том случае, если дальше по коду вам предстоит еще хотя бы раз сравнивать IEnumerable<FileInfo>.   На этом у меня все, надеюсь, что читатель нашел любопытным мой скромный труд. Хочется выразить огромную благодарность моей жене за помощь в подготовке данного поста, а также сообществу .NET Talks🎄 Программист