在属性上筛选Linq EXCEPT

本文关键字:Linq EXCEPT 筛选 属性 | 更新日期: 2023-09-27 18:04:55

这可能看起来很傻,但我在linq中发现的所有使用Except的示例都使用了两个仅包含字符串或整数的列表或数组,并根据匹配项对其进行筛选,例如:

var excludes = users.Except(matches);

我想使用exclude来保持我的代码简短,但似乎找不到如何做到以下几点:

class AppMeta
{
    public int Id { get; set; }
}
var excludedAppIds = new List<int> {2, 3, 5, 6};
var unfilteredApps = new List<AppMeta>
                         {
                           new AppMeta {Id = 1},
                           new AppMeta {Id = 2},
                           new AppMeta {Id = 3},
                           new AppMeta {Id = 4},
                           new AppMeta {Id = 5}
                         }

如何获取在excludedAppIds上进行筛选的AppMeta列表?

在属性上筛选Linq EXCEPT

尝试一个简单的where查询

var filtered = unfilteredApps.Where(i => !excludedAppIds.Contains(i.Id)); 

except方法使用相等,您的列表包含不同类型的对象,因此它们包含的项都不相等!

ColinE的回答简洁而优雅。如果您的列表较大,并且排除的应用程序列表已排序,则BinarySearch<T>可能比Contains更快。

示例:

unfilteredApps.Where(i => excludedAppIds.BinarySearch(i.Id) < 0);

我为Except使用了一个扩展方法,该方法允许您比较苹果和橙子,只要它们都有可以用来比较的共同点,比如Id或Key。

public static class ExtensionMethods
{
    public static IEnumerable<TA> Except<TA, TB, TK>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TK> selectKeyA,
        Func<TB, TK> selectKeyB, 
        IEqualityComparer<TK> comparer = null)
    {
        return a.Where(aItem => !b.Select(bItem => selectKeyB(bItem)).Contains(selectKeyA(aItem), comparer));
    }
}

然后像这样使用它:

var filteredApps = unfilteredApps.Except(excludedAppIds, a => a.Id, b => b);

这个扩展与ColinE的答案非常相似,它只是被打包成一个整洁的扩展,可以重复使用,而不需要太多的脑力开销。

这就是LINQ需要的地方

public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKey) 
{
    return from item in items
            join otherItem in other on getKey(item)
            equals getKey(otherItem) into tempItems
            from temp in tempItems.DefaultIfEmpty()
            where ReferenceEquals(null, temp) || temp.Equals(default(T))
            select item; 
}

从排除的列表中构造一个List<AppMeta>,并使用Except-Linq运算符。

var ex = excludedAppIds.Select(x => new AppMeta{Id = x}).ToList();                           
var result = ex.Except(unfilteredApps).ToList();

由于.NET 6,现在有一个内置的ExceptBy专门为此设计:

var filtered = unfilteredApps.ExceptBy(excludedAppIds, a => a.Id); 

我喜欢Except扩展方法,但最初的问题没有对称密钥访问,我更喜欢Contains(或Any变体(来加入,所以所有的功劳都归功于azuneca的答案:

public static IEnumerable<T> Except<T, TKey>(this IEnumerable<TKey> items,
    IEnumerable<T> other, Func<T, TKey> getKey) {
    return from item in items
        where !other.Contains(getKey(item))
        select item;
}

然后可以像这样使用:

var filteredApps = unfilteredApps.Except(excludedAppIds, ua => ua.Id);

此外,此版本允许使用Select:为异常IEnumerable进行映射

var filteredApps = unfilteredApps.Except(excludedApps.Select(a => a.Id), ua => ua.Id);

MoreLinq对此有一些有用的东西MoreLinq.Source.MoreEnumerable.ExceptBy

https://github.com/gsscoder/morelinq/blob/master/MoreLinq/ExceptBy.cs

namespace MoreLinq
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    static partial class MoreEnumerable
    {
        /// <summary>
        /// Returns the set of elements in the first sequence which aren't
        /// in the second sequence, according to a given key selector.
        /// </summary>
        /// <remarks>
        /// This is a set operation; if multiple elements in <paramref name="first"/> have
        /// equal keys, only the first such element is returned.
        /// This operator uses deferred execution and streams the results, although
        /// a set of keys from <paramref name="second"/> is immediately selected and retained.
        /// </remarks>
        /// <typeparam name="TSource">The type of the elements in the input sequences.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
        /// <param name="first">The sequence of potentially included elements.</param>
        /// <param name="second">The sequence of elements whose keys may prevent elements in
        /// <paramref name="first"/> from being returned.</param>
        /// <param name="keySelector">The mapping from source element to key.</param>
        /// <returns>A sequence of elements from <paramref name="first"/> whose key was not also a key for
        /// any element in <paramref name="second"/>.</returns>
        public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first,
            IEnumerable<TSource> second,
            Func<TSource, TKey> keySelector)
        {
            return ExceptBy(first, second, keySelector, null);
        }
        /// <summary>
        /// Returns the set of elements in the first sequence which aren't
        /// in the second sequence, according to a given key selector.
        /// </summary>
        /// <remarks>
        /// This is a set operation; if multiple elements in <paramref name="first"/> have
        /// equal keys, only the first such element is returned.
        /// This operator uses deferred execution and streams the results, although
        /// a set of keys from <paramref name="second"/> is immediately selected and retained.
        /// </remarks>
        /// <typeparam name="TSource">The type of the elements in the input sequences.</typeparam>
        /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
        /// <param name="first">The sequence of potentially included elements.</param>
        /// <param name="second">The sequence of elements whose keys may prevent elements in
        /// <paramref name="first"/> from being returned.</param>
        /// <param name="keySelector">The mapping from source element to key.</param>
        /// <param name="keyComparer">The equality comparer to use to determine whether or not keys are equal.
        /// If null, the default equality comparer for <c>TSource</c> is used.</param>
        /// <returns>A sequence of elements from <paramref name="first"/> whose key was not also a key for
        /// any element in <paramref name="second"/>.</returns>
        public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first,
            IEnumerable<TSource> second,
            Func<TSource, TKey> keySelector,
            IEqualityComparer<TKey> keyComparer)
        {
            if (first == null) throw new ArgumentNullException("first");
            if (second == null) throw new ArgumentNullException("second");
            if (keySelector == null) throw new ArgumentNullException("keySelector");
            return ExceptByImpl(first, second, keySelector, keyComparer);
        }
        private static IEnumerable<TSource> ExceptByImpl<TSource, TKey>(this IEnumerable<TSource> first,
            IEnumerable<TSource> second,
            Func<TSource, TKey> keySelector,
            IEqualityComparer<TKey> keyComparer)
        {
            var keys = new HashSet<TKey>(second.Select(keySelector), keyComparer);
            foreach (var element in first)
            {
                var key = keySelector(element);
                if (keys.Contains(key))
                {
                    continue;
                }
                yield return element;
                keys.Add(key);
            }
        }
    }
}
public static class ExceptByProperty
{
    public static List<T> ExceptBYProperty<T, TProperty>(this List<T> list, List<T> list2, Expression<Func<T, TProperty>> propertyLambda)
    {
        Type type = typeof(T);
        MemberExpression member = propertyLambda.Body as MemberExpression;
        if (member == null)
            throw new ArgumentException(string.Format(
                "Expression '{0}' refers to a method, not a property.",
                propertyLambda.ToString()));
        PropertyInfo propInfo = member.Member as PropertyInfo;
        if (propInfo == null)
            throw new ArgumentException(string.Format(
                "Expression '{0}' refers to a field, not a property.",
                propertyLambda.ToString()));
        if (type != propInfo.ReflectedType &&
            !type.IsSubclassOf(propInfo.ReflectedType))
            throw new ArgumentException(string.Format(
                "Expresion '{0}' refers to a property that is not from type {1}.",
                propertyLambda.ToString(),
                type));
        Func<T, TProperty> func = propertyLambda.Compile();
        var ids = list2.Select<T, TProperty>(x => func(x)).ToArray();
        return list.Where(i => !ids.Contains(((TProperty)propInfo.GetValue(i, null)))).ToList();
    }
}
public class testClass
{
    public int ID { get; set; }
    public string Name { get; set; }
}

用于测试:

        List<testClass> a = new List<testClass>();
        List<testClass> b = new List<testClass>();
        a.Add(new testClass() { ID = 1 });
        a.Add(new testClass() { ID = 2 });
        a.Add(new testClass() { ID = 3 });
        a.Add(new testClass() { ID = 4 });
        a.Add(new testClass() { ID = 5 });
        b.Add(new testClass() { ID = 3 });
        b.Add(new testClass() { ID = 5 });
        a.Select<testClass, int>(x => x.ID);
        var items = a.ExceptBYProperty(b, u => u.ID);