林克:使用 .Except() 通过使它们可转换/可比较来对不同类型的集合进行

本文关键字:可比较 同类型 集合 可转换 Except 使用 林克 | 更新日期: 2023-09-27 18:29:57

给定两个不同类型的列表,是否可以使这些类型彼此可转换或相互比较(例如,使用TypeConverter或类似类型(,以便LINQ查询可以比较它们? 我在 SO 上看到了其他类似的问题,但没有指出使类型彼此可转换以解决问题。

馆藏类型:

public class Data
{
    public int ID { get; set; }
}
public class ViewModel
{
    private Data _data;
    public ViewModel(Data data)
    {
        _data = data;
    }
}

期望用法:

    public void DoMerge(ObservableCollection<ViewModel> destination, IEnumerable<Data> data)
    {
        // 1. Find items in data that don't already exist in destination
        var newData = destination.Except(data);
        // ...
    }

这似乎是合乎逻辑的,因为我知道如何将 ViewModel 的实例与 Data 的实例进行比较,我应该能够提供一些比较逻辑,然后 LINQ 将使用这些逻辑进行查询,例如 。除了((。 这可能吗?

林克:使用 .Except() 通过使它们可转换/可比较来对不同类型的集合进行

我知道

这已经晚了,但是使用 Func 有一种更简单的语法,无需比较器。

public static class LinqExtensions
{
   public static IEnumerable<TSource> Except<TSource, VSource>(this IEnumerable<TSource> first, IEnumerable<VSource> second, Func<TSource, VSource, bool> comparer)
   {
       return first.Where(x => second.Count(y => comparer(x, y)) == 0);
   }
   public static IEnumerable<TSource> Contains<TSource, VSource>(this IEnumerable<TSource> first, IEnumerable<VSource> second, Func<TSource, VSource, bool> comparer)
   {
       return first.Where(x => second.FirstOrDefault(y => comparer(x, y)) != null);
   }
   public static IEnumerable<TSource> Intersect<TSource, VSource>(this IEnumerable<TSource> first, IEnumerable<VSource> second, Func<TSource, VSource, bool> comparer)
   {
       return first.Where(x => second.Count(y => comparer(x, y)) == 1);
   }
}

所以有类 Foo 和 Bar 的列表

public class Bar
{
   public int Id { get; set; }
   public string OtherBar { get; set; }
}
public class Foo
{
   public int Id { get; set; }
   public string OtherFoo { get; set; }
}

可以运行 Linq 语句,例如

var fooExceptBar = fooList.Except(barList, (f, b) => f.Id == b.Id);
var barExceptFoo = barList.Except(fooList, (b, f) => b.OtherBar == f.OtherFoo);

它基本上是上面的轻微变化,但对我来说似乎更干净。

我认为提供从DataViewModel的投影是有问题的,因此除了Jason的解决方案之外,我还提供了另一种解决方案。

除了使用哈希集(如果我没记错的话(,因此您可以通过创建自己的哈希集来获得类似的性能。 我还假设您在Data对象的IDs相等时将其标识为相等。

var oldIDs = new HashSet<int>(data.Select(d => d.ID));
var newData = destination.Where(vm => !oldIDs.Contains(vm.Data.ID));

您可能在方法中的其他位置有"oldData"集合的另一种用途,在这种情况下,您可能希望改为执行此操作。 在数据类上实现IEquatable<Data>,或为哈希集创建自定义IEqualityComparer<Data>

var oldData = new HashSet<Data>(data);
//or: var oldData = new HashSet<Data>(data, new DataEqualityComparer());
var newData = destination.Where(vm => !oldData.Contains(vm.Data));

如果你使用这个:

var newData = destination.Except(data.Select(x => f(x)));

您必须将"数据"投影到"目标"中包含的相同类型,但是使用以下代码可以摆脱此限制:

//Here is how you can compare two different sets.
class A { public string Bar { get; set; } }
class B { public string Foo { get; set; } }
IEnumerable<A> setOfA = new A[] { /*...*/ };
IEnumerable<B> setOfB = new B[] { /*...*/ };
var subSetOfA1 = setOfA.Except(setOfB, a => a.Bar, b => b.Foo);
//alternatively you can do it with a custom EqualityComparer, if your not case sensitive for instance.
var subSetOfA2 = setOfA.Except(setOfB, a => a.Bar, b => b.Foo, StringComparer.OrdinalIgnoreCase);
//Here is the extension class definition allowing you to use the code above
public static class IEnumerableExtension
{
    public static IEnumerable<TFirst> Except<TFirst, TSecond, TCompared>(
        this IEnumerable<TFirst> first,
        IEnumerable<TSecond> second,
        Func<TFirst, TCompared> firstSelect,
        Func<TSecond, TCompared> secondSelect)
    {
        return Except(first, second, firstSelect, secondSelect, EqualityComparer<TCompared>.Default);
    }
    public static IEnumerable<TFirst> Except<TFirst, TSecond, TCompared>(
        this IEnumerable<TFirst> first,
        IEnumerable<TSecond> second,
        Func<TFirst, TCompared> firstSelect,
        Func<TSecond, TCompared> secondSelect,
        IEqualityComparer<TCompared> comparer)
    {
        if (first == null)
            throw new ArgumentNullException("first");
        if (second == null)
            throw new ArgumentNullException("second");
        return ExceptIterator<TFirst, TSecond, TCompared>(first, second, firstSelect, secondSelect, comparer);
    }
    private static IEnumerable<TFirst> ExceptIterator<TFirst, TSecond, TCompared>(
        IEnumerable<TFirst> first,
        IEnumerable<TSecond> second,
        Func<TFirst, TCompared> firstSelect,
        Func<TSecond, TCompared> secondSelect,
        IEqualityComparer<TCompared> comparer)
    {
        HashSet<TCompared> set = new HashSet<TCompared>(second.Select(secondSelect), comparer);
        foreach (TFirst tSource1 in first)
            if (set.Add(firstSelect(tSource1)))
                yield return tSource1;
    }
}

有些人可能会争辩说,由于使用了HashSet,内存效率低下。但实际上框架的 Enumerable.Except 方法对一个名为"Set"的类似内部类做同样的事情(我通过反编译查看了一下(。

最好的办法是提供从DataViewModel的投影,以便您可以说

var newData = destination.Except(data.Select(x => f(x)));

f映射到ViewModel Data的位置。您还需要IEqualityComparer<Data>

肮脏的伎俩:将两个列表投影到它们的共享 ID,执行Except,然后使用带有原始列表的Join将结果列表再次投影到原始类型,以添加您在第一个Select中删除的其余属性。可能性能不是很好,但对于小列表来说还可以。

listOfType1.Select(x => x.Id)
    .Except(listOfType2.Select(x => x.Id))
    .Join(listOfType1,
        onlyIds => onlyIds,
        fullData => fullData.Id,
        (onlyIds, fullData)
        => new Type1
        {
            Id = fullData.Id,
            OtherPropertyOfType1 = fullData.OtherPropertyOfType1
        });