Кроме того, имеет сходный эффект с отличием?

Я только что обнаружил, что Except() удалит все элементы во втором списке из первого, но также имеет эффект, который делает все элементы в возвращаемом результате различными.

Простой способ, которым я пользуюсь, - Where(v => !secondList.Contains(v))

Может кто-нибудь объяснить мне, почему это поведение, и, если возможно, указать мне на документацию, которая объясняет это?

Ответ 1

Документация для функции Except гласит:

Производит заданное различие двух последовательностей с помощью сравнения по умолчанию для сравнения значений.

Установленное различие двух наборов определяется как члены первого набора, которые не отображаются во втором наборе.

Важным словом здесь является set, который определяется как:

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

Поскольку Except документируется как операция на основе набора, это также приводит к тому, что результирующие значения различаются.

Ответ 2

Вы писали:

Простой способ, которым я пользуюсь, - Where(v => !secondList.Contains(v))

Когда вы это сделаете, все еще выполняется Distict с помощью secondList.

Например:

var firstStrings = new [] { "1", null, null, null, "3", "3" };
var secondStrings = new [] { "1", "1", "1", null, null, "4" };
var resultStrings = firstStrings.Where(v => !secondStrings.Contains(v)); // 3, 3  

Я создал метод расширения, который не имеет никакого отличия. Пример использования:

var result2Strings = firstStrings.ExceptAll(secondStrings).ToList(); // null, 3, 3

Это то, что он делает:

enter image description here

Это источник:

public static IEnumerable<TSource> ExceptAll<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second)
{
    // Do not call reuse the overload method because that is a slower imlementation
    if (first == null) { throw new ArgumentNullException("first"); }
    if (second == null) { throw new ArgumentNullException("second"); }

    var secondList = second.ToList();
    return first.Where(s => !secondList.Remove(s));
}

public static IEnumerable<TSource> ExceptAll<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    IEqualityComparer<TSource> comparer)
{
    if (first == null) { throw new ArgumentNullException("first"); }
    if (second == null) { throw new ArgumentNullException("second"); }
    var comparerUsed = comparer ?? EqualityComparer<TSource>.Default;

    var secondList = second.ToList();
    foreach (var item in first)
    {
        if (secondList.Contains(item, comparerUsed))
        {
            secondList.Remove(item);
        }
        else
        {
            yield return item;
        }
    }
}

Изменить: более быстрая реализация, основанная на комментарии DigEmAll

public static IEnumerable<TSource> ExceptAll<TSource>(
        this IEnumerable<TSource> first,
        IEnumerable<TSource> second)
{
    return ExceptAll(first, second, null);
}

public static IEnumerable<TSource> ExceptAll<TSource>(
    this IEnumerable<TSource> first,
    IEnumerable<TSource> second,
    IEqualityComparer<TSource> comparer)
{
    if (first == null) { throw new ArgumentNullException("first"); }
    if (second == null) { throw new ArgumentNullException("second"); }


    var secondCounts = new Dictionary<TSource, int>(comparer ?? EqualityComparer<TSource>.Default);
    int count;
    int nullCount = 0;

    // Count the values from second
    foreach (var item in second)
    {
        if (item == null)
        {
            nullCount++;
        }
        else
        {
            if (secondCounts.TryGetValue(item, out count))
            {
                secondCounts[item] = count + 1;
            }
            else
            {
                secondCounts.Add(item, 1);
            } 
        }
    }

    // Yield the values from first
    foreach (var item in first)
    {
        if (item == null)
        {
            nullCount--;
            if (nullCount < 0)
            {
                yield return item;
            } 
        }
        else
        {
            if (secondCounts.TryGetValue(item, out count))
            {
                if (count == 0)
                {
                    secondCounts.Remove(item);
                    yield return item;
                }
                else
                {
                    secondCounts[item] = count - 1;
                }
            }
            else
            {
                yield return item;
            }
        }
    }
}

Дополнительная информация в моем блоге (также вариант для Intersect и Union)