简体   繁体   中英

Can I use Linq's Except() with a lambda expression comparer?

I know I can call linq's Except and specify a custom IEqualityComparer, but implementing a new Comparer class for each data type seems like an overkill for this purpose. Can I use a lambda expression to provide the equality function, like when I use Where or other LINQ functions?

If I can't, is there an alternative?

For any one still looking; here's another way of implementing a custom lambda comparer.

public class LambdaComparer<T> : IEqualityComparer<T>
    {
        private readonly Func<T, T, bool> _expression;

        public LambdaComparer(Func<T, T, bool> lambda)
        {
            _expression = lambda;
        }

        public bool Equals(T x, T y)
        {
            return _expression(x, y);
        }

        public int GetHashCode(T obj)
        {
            /*
             If you just return 0 for the hash the Equals comparer will kick in. 
             The underlying evaluation checks the hash and then short circuits the evaluation if it is false.
             Otherwise, it checks the Equals. If you force the hash to be true (by assuming 0 for both objects), 
             you will always fall through to the Equals check which is what we are always going for.
            */
            return 0;
        }
    }

you can then create an extension for the linq Except an Intersect that take in lambda's

/// <summary>
        /// Returns all items in the first collection except the ones in the second collection that match the lambda condition
        /// </summary>
        /// <typeparam name="T">The type</typeparam>
        /// <param name="listA">The first list</param>
        /// <param name="listB">The second list</param>
        /// <param name="lambda">The filter expression</param>
        /// <returns>The filtered list</returns>
        public static IEnumerable<T> Except<T>(this IEnumerable<T> listA, IEnumerable<T> listB, Func<T, T, bool> lambda)
        {
            return listA.Except(listB, new LambdaComparer<T>(lambda));
        }

        /// <summary>
        /// Returns all items in the first collection that intersect the ones in the second collection that match the lambda condition
        /// </summary>
        /// <typeparam name="T">The type</typeparam>
        /// <param name="listA">The first list</param>
        /// <param name="listB">The second list</param>
        /// <param name="lambda">The filter expression</param>
        /// <returns>The filtered list</returns>
        public static IEnumerable<T> Intersect<T>(this IEnumerable<T> listA, IEnumerable<T> listB, Func<T, T, bool> lambda)
        {
            return listA.Intersect(listB, new LambdaComparer<T>(lambda));
        }

Usage:

var availableItems = allItems.Except(filterItems, (p, p1) => p.Id== p1.Id);

Can you not use a.Where with a lambda that filters out your required values?

Example as requested:

    static void Main(string[] args)
    {
        var firstCustomers = new[] { new Customer { Id = 1, Name = "Bob" }, new Customer { Id = 2, Name = "Steve" } };
        var secondCustomers = new[] { new Customer { Id = 2, Name = "Steve" }, new Customer { Id = 3, Name = "John" } };

        var customers = secondCustomers.Where(c => !firstCustomers.Select(fc => fc.Id).Contains(c.Id));
    }

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

I don't think you can directly with the basic LINQ interfaces, but I've seen people implement a LambdaComparer class with extension methods which will help you do it.

Here's an example

Here's something simple I whipped up:

public class CustomComparer<TSource, TCompareType> : IEqualityComparer<TSource> where TSource : class 
{
    private readonly Func<TSource, TCompareType> getComparisonObject;
    public CustomComparer(Func<TSource,TCompareType> getComparisonObject)
    {
        if (getComparisonObject == null) throw new ArgumentNullException("getComparisonObject");
        this.getComparisonObject = getComparisonObject;
    } 

    /// <summary>
    /// Determines whether the specified objects are equal.
    /// </summary>
    /// <returns>
    /// true if the specified objects are equal; otherwise, false.
    /// </returns>
    /// <param name="x">The first object of type <paramref name="T"/> to compare.
    ///                 </param><param name="y">The second object of type <paramref name="T"/> to compare.
    ///                 </param>
    public bool Equals(TSource x, TSource y)
    {
        if (x == null)
        {
            return (y == null);
        }
        else if (y == null)
        {
            return false;
        }
        return EqualityComparer<TCompareType>.Default.Equals(getComparisonObject(x), getComparisonObject(y));
    }

    /// <summary>
    /// Returns a hash code for the specified object.
    /// </summary>
    /// <returns>
    /// A hash code for the specified object.
    /// </returns>
    /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.
    ///                 </param><exception cref="T:System.ArgumentNullException">The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null.
    ///                 </exception>
    public int GetHashCode(TSource obj)
    {
        return EqualityComparer<TCompareType>.Default.GetHashCode(getComparisonObject(obj));
    }
}

Usage:

var myItems = allItems.Except(theirItems, new CustomComparer(item => item.Name));

Use an extension !

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;

}

Source

Here' is l except r solution, based on a LINQ outer join technique:

from l in new[] { 1, 2, 3 }
join r in new[] { 2, 4, 5 }
on l equals r
into rr
where !rr.Any()
select l

Will yield: 1, 3

But you may try this. This is so much easy, I think code contains errors. Of course, the code is for small amounts, without LINQ translates to db etc.

public static IEnumerable<TSource> ExceptPredicate<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TSource, bool> compare) {
  foreach (var itmFirst in first) {
    if (!second.Any(itmsecond => compare(itmFirst, itmsecond))) {
      yield return itmFirst;
    }
  }
  yield break;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM