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

dsekswqp  于 2023-09-28  发布在  其他
关注(0)|答案(5)|浏览(98)

给定两个不同类型的列表,是否有可能使这些类型相互转换或相互比较(例如使用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()之类的查询,这似乎是合乎逻辑的。这可能吗?

fjaof16o

fjaof16o1#

我知道这有点晚了,但是使用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);

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

yrwegjxp

yrwegjxp2#

我假设提供从DataViewModel的投影是有问题的,所以我提供了除Jason的解决方案之外的另一个解决方案。
Except使用散列集(如果我没记错的话),所以您可以通过创建自己的散列集来获得类似的性能。我还假设当IDs相等时,您将Data对象标识为相等。

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));
vtwuwzda

vtwuwzda3#

如果您使用此:

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

您必须将'data'投影到'destination'中包含的相同类型,但使用下面的代码可以摆脱此限制:

//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,内存效率低下。但实际上,框架的Except方法正在用一个类似的内部类“Set”(我通过反编译看了一下)做同样的事情。

holgip5t

holgip5t4#

最好的办法是提供一个从DataViewModel的投影,这样您就可以说

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

其中fDataMap到ViewModel。你也需要一个IEqualityComparer<Data>

rsl1atfo

rsl1atfo5#

肮脏的把戏:将两个列表投影到它们的共享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
        });

相关问题