Reputation: 47377
How do I get itemsToRemove
to only contain "bar one", and itemsToAdd
to only contain "bar five"?
I'm trying to use "Except", but obviously I'm using it incorrectly.
var oldList = new List<Foo>();
oldList.Add(new Foo(){ Bar = "bar one"});
oldList.Add(new Foo(){ Bar = "bar two"});
oldList.Add(new Foo(){ Bar = "bar three"});
oldList.Add(new Foo(){ Bar = "bar four"});
var newList = new List<Foo>();
newList.Add(new Foo(){ Bar = "bar two"});
newList.Add(new Foo(){ Bar = "bar three"});
newList.Add(new Foo(){ Bar = "bar four"});
newList.Add(new Foo(){ Bar = "bar five"});
var itemsToRemove = oldList.Except(newList); // should only contain "bar one"
var itemsToAdd = newList.Except(oldList); // should only contain "bar one"
foreach(var item in itemsToRemove){
Console.WriteLine(item.Bar + " removed");
// currently says
// bar one removed
// bar two removed
// bar three removed
// bar four removed
}
foreach(var item in itemsToAdd){
Console.WriteLine(item.Bar + " added");
// currently says
// bar two added
// bar three added
// bar four added
// bar five added
}
Upvotes: 1
Views: 162
Reputation: 39950
This is mostly a riff on Servy's answer to give a more general approach to this:
public class PropertyEqualityComparer<TItem, TKey> : EqualityComparer<Tuple<TItem, TKey>>
{
readonly Func<TItem, TKey> _getter;
public PropertyEqualityComparer(Func<TItem, TKey> getter)
{
_getter = getter;
}
public Tuple<TItem, TKey> Wrap(TItem item) {
return Tuple.Create(item, _getter(item));
}
public TItem Unwrap(Tuple<TItem, TKey> tuple) {
return tuple.Item1;
}
public override bool Equals(Tuple<TItem, TKey> x, Tuple<TItem, TKey> y)
{
if (x.Item2 == null && y.Item2 == null) return true;
if (x.Item2 == null || y.Item2 == null) return false;
return x.Item2.Equals(y.Item2);
}
public override int GetHashCode(Tuple<TItem, TKey> obj)
{
if (obj.Item2 == null) return 0;
return obj.Item2.GetHashCode();
}
}
public static class ComparerLinqExtensions {
public static IEnumerable<TSource> Except<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keyGetter)
{
var comparer = new PropertyEqualityComparer<TSource, TKey>(keyGetter);
var firstTuples = first.Select(comparer.Wrap);
var secondTuples = second.Select(comparer.Wrap);
return firstTuples.Except(secondTuples, comparer)
.Select(comparer.Unwrap);
}
}
// ...
var itemsToRemove = oldList.Except(newList, foo => foo.Bar);
var itemsToAdd = newList.Except(oldList, foo => foo.Bar);
This should work fine for any classes without unusual equality semantics, where it's incorrect to call the object.Equals()
override instead of IEquatable<T>.Equals()
.Notably, this will work fine for anonymous types.
Upvotes: 1
Reputation: 203821
Except
will use the default Equals
and GetHashCode
method of the objects in question to define "equality" for the objects, unless you provide a custom comparer (you have not). In this case, that will compare the references of the objects, not their Bar
value.
One option would be to create an IEqualityComparer<Foo>
that compares the Bar
property, rather than references to the object itself.
public class FooComparer : IEqualityComparer<Foo>
{
public bool Equals(Foo x, Foo y)
{
if (x == null ^ y == null)
return false;
if (x == null && y == null)
return true;
return x.Bar == y.Bar;
}
public int GetHashCode(Foo obj)
{
if (obj == null)
return 0;
return obj.Bar.GetHashCode();
}
}
Another option is to create an Except
method that accepts a selector to compare the values on. We can create such a method and then use that:
public static IEnumerable<TSource> ExceptBy<TSource, TKey>(
this IEnumerable<TSource> first,
IEnumerable<TSource> second,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer = null)
{
comparer = comparer ?? EqualityComparer<TKey>.Default;
var set = new HashSet<TKey>(second.Select(keySelector), comparer);
return first.Where(item => set.Add(keySelector(item)));
}
This allows us to write:
var itemsToRemove = oldList.ExceptBy(newList, foo => foo.Bar);
var itemsToAdd = newList.ExceptBy(oldList, foo => foo.Bar);
Upvotes: 7
Reputation: 4094
Implement IComparable on your data objects; I think you're being bitten by reference comparison. If you change Foo to just string, your code works.
var oldList = new List<string>();
oldList.Add("bar one");
oldList.Add("bar two");
oldList.Add("bar three");
oldList.Add("bar four");
var newList = new List<string>();
newList.Add("bar two");
newList.Add("bar three");
newList.Add("bar four");
newList.Add("bar five");
var itemsToRemove = oldList.Except(newList); // should only contain "bar one"
var itemsToAdd = newList.Except(oldList); // should only contain "bar one"
foreach (var item in itemsToRemove)
{
Console.WriteLine(item + " removed");
}
foreach (var item in itemsToAdd)
{
Console.WriteLine(item + " added");
}
Upvotes: 0
Reputation: 14870
Your logic is sound, but Except
default behaviour for comparing two classes is to go by references. Since you are effectively create two lists with 8 differents objets (regardless of their content), there will be no two equal objects.
You can, however, use the Except
overload that takes an IEqualityComparer. For example:
public class FooEqualityComparer : IEqualityComparer<Foo>
{
public bool Equals(Foo left, Foo right)
{
if(left == null && right == null) return true;
return left != null && right != null && left.Bar == right.Bar;
}
public int GetHashCode(Foo item)
{
return item != null ? item.Bar.GetHashcode() : 0;
}
}
// In your code
var comparer = new FooEqualityComparer();
var itemsToRemove = oldList.Except(newList, comparer );
var itemsToAdd = newList.Except(oldList, comparer);
Upvotes: 2
Reputation: 34772
This is because you're comparing objects of type Foo
, and not property Bar of type string
. Try:
var itemsToRemove = oldList.Select(i => i.Bar).Except(newList.Select(i => i.Bar));
var itemsToAdd = newList.Select(i => i.Bar).Except(oldList.Select(i => i.Bar));
Upvotes: 0