-2

I have the following two lists:

List<MyObject> oldList = new List<MyObject>();
oldList.Add(new MyObject { Id = 1, Name = "hello" });
oldList.Add(New MyObject { Id = 2, Name = "world" });

List<MyObject> newList = new List<MyObject>();
newList.Add(new MyObject { Id = 1, Name = "Hello" });
newList.Add(new MyObject { Id = 3, Name = "World" });
newList.Add(new MyObject { Id = 4, Name = "hello" });

I would like to write something to compare the two lists and return boolean true if the lists are different.

For example, the lists above are different in the following ways:

  • Id's don't exactly match
  • Counts don't match
  • In cases where Id's do match, the Name's don't exactly match (case-sensitive)

I have tried the following:

if (oldList != newList)

And:

if (!oldList.SequenceEqual(newList))

However, both produce inaccurate results. I understand I can create a class of type IEqualityComparer which implements a HashSet comparison; but I also read that it may not work for my case... Can someone shed any light on how to compare two objects to detect the types of changes I have specified?

user1477388
  • 20,790
  • 32
  • 144
  • 264
  • 3
    You're going to need to *show us the comparer* if it's not functioning properly. – Servy Mar 16 '15 at 14:08
  • 2
    _"I also read that it may not work for my case..."_ - I beg to differ. Show that you have implemented and understood comparison. Explain why you think it wouldn't work. – CodeCaster Mar 16 '15 at 14:08
  • There are many ways you could compare the two lists... Is order important? Do the objects have a "primary key" (like id)? How should the differences be listed? – xanatos Mar 16 '15 at 14:09
  • @Servy I haven't implemented the comparer yet because of the doubts that I've mentioned. Are you saying a comparer is the way to go here (despite what I've read to the contrary)? – user1477388 Mar 16 '15 at 14:09
  • @CodeCaster Well, `if (oldList != newList)` isn't going to work, even with a custom comparer... – Servy Mar 16 '15 at 14:09
  • @Servy It can be used to exclude self-checking (x == x). I do prefer `if (!object.ReferenceEquals(oldList, newList))` – xanatos Mar 16 '15 at 14:10
  • 2
    @user1477388 `SequenceEqual` can tell you if the sequences are equal, assuming you implement equality as you desire, and you want order to matter. – Servy Mar 16 '15 at 14:11
  • But `SequenceEqual` clearly won't tell you the difference! It is very "binary"... Equal or not equal. – xanatos Mar 16 '15 at 14:11
  • 1
    @xanatos The point is that it isn't going to have the desired semantics, and there's no way to change that just using the `==` operator. As to your last comment: "I would like to write something to compare the two lists and **return boolean true if the lists are different**." – Servy Mar 16 '15 at 14:12
  • _"I haven't implemented the comparer yet because of the doubts that I've mentioned"_ - is _"I also read that it may not work for my case"_ your "doubt"? Explain specifically what you read and why you think that won't work. – CodeCaster Mar 16 '15 at 14:16
  • @Servy I had ordered the list by the same key and attempted to use the default SequenceEqual(), not implementing my own type. I guess after what I read, I wasn't sure how to write the IEqualityComparer to meet my needs. Can you provide an example of what it should look like. All the ones I saw performed a `HashSet` comparison which seemed to compare the "reference type" meaning that it was the exact same reference in memory (I think). I just want to compare the properties and the orders of those properties to see if there are any discrepancies. – user1477388 Mar 16 '15 at 14:16
  • 1
    @user1477388 All you need to do is write a method that determines when two objects are actually equal, given whatever definition of equality you want to use. That's it. If you've written an implementation and are having problems, you can show us what it is. If you haven't even tried, you have no problem for us to solve. – Servy Mar 16 '15 at 14:18

1 Answers1

1

As you've said, you only need to implement the correct IEquatable<MyObject> interface in your MyObject, or implement an IEqualityComparer<MyObject>

/// <summary>
/// Fully equatable MyObject
/// </summary>
public class MyObject : IEquatable<MyObject>
{
    public int Id { get; set; }
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        // obj is object, so we can use its == operator
        if (obj == null)
        {
            return false;
        }

        MyObject other = obj as MyObject;

        if (object.ReferenceEquals(other, null))
        {
            return false;
        }

        return this.InnerEquals(other);
    }

    public bool Equals(MyObject other)
    {
        if (object.ReferenceEquals(other, null))
        {
            return false;
        }

        return this.InnerEquals(other);
    }

    private bool InnerEquals(MyObject other)
    {
        // Here we know that other != null;

        if (object.ReferenceEquals(this, other))
        {
            return true;
        }

        return this.Id == other.Id && this.Name == other.Name;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            // From http://stackoverflow.com/a/263416/613130
            int hash = 17;
            hash = hash * 23 + this.Id.GetHashCode();
            hash = hash * 23 + (this.Name != null ? this.Name.GetHashCode() : 0);
            return hash;
        }
    }
}

and then you can use

if (!oldList.SequenceEqual(newList))

Note that this will compare element order! If you change element order, then the comparison will return false

Or you can use an "external" IEqualityComparer<MyObject>

public class MyObjectEqualityComparer : IEqualityComparer<MyObject>
{
    public static readonly MyObjectEqualityComparer Default = new MyObjectEqualityComparer();

    protected MyObjectEqualityComparer()
    {
    }

    public bool Equals(MyObject x, MyObject y)
    {
        if (object.ReferenceEquals(x, null))
        {
            return object.ReferenceEquals(y, null);
        }

        if (object.ReferenceEquals(y, null))
        {
            return false;
        }

        // Here we know that x != null && y != null;

        if (object.ReferenceEquals(x, y))
        {
            return true;
        }

        return x.Id == y.Id && x.Name == y.Name;
    }

    public int GetHashCode(MyObject obj)
    {
        if (obj == null)
        {
            return 0;
        }

        unchecked
        {
            // From http://stackoverflow.com/a/263416/613130
            int hash = 17;
            hash = hash * 23 + obj.Id.GetHashCode();
            hash = hash * 23 + (obj.Name != null ? obj.Name.GetHashCode() : 0);
            return hash;
        }
    }
}

use it like

if (!oldList.SequenceEqual(newList, MyObjectEqualityComparer.Default))

Note that there are various schools of thought on how exactly to write equality comparers, and there is a little caveat: if you override the operator== you must be very very wary of not using it inside your comparator :-)

A classical example of wrong code, when the operator== is overloaded

public bool Equals(MyObject other)
{
    // BOOOM!!! StackOverflowException!
    // Equals will call operator== that will probably call
    // Equals back! and so on and so on.
    if (other == null)
    {
        return false;
    }

    return this.InnerEquals(other);
}

So it is better to do object.ReferenceEquals(something, someotherthing) that does reference comparison.

Then there is the problem of null handling with properties:

The hash of Name (a string) is written like this:

hash = hash * 23 + obj.Name != null ? obj.Name.GetHashCode() : 0

so that if obj.Name is null the code doesn't expode in a NullReferenceException. Automatically generated code for anonymous objects use another way:

hash = hash * 23 + EqualityComparer<string>.Default.GetHashCode(obj.Name);

The EqualityComparer<string>.Default is safe to use, even with null values.

For the Equals comparison of properties, the automatically generated code for anonymous objects uses another funny trick:

&& EqualityComparer<string>.Default.Equals(this.Name, obj.Name);

so it uses EqualityComparer<string>.Default.Equals, that then correctly uses the various methods/interfaces to compare objects.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • I've seen that argument before about `==` but how would that overloaded `==` operator look to cause that problem yet still cope with `null`s itself? – weston Mar 16 '15 at 14:41
  • See the example here: https://msdn.microsoft.com/en-us/library/ms131190(v=vs.110).aspx It's interesting because it overloads `==` yet **doesn't** use `Object.ReferenceEquals` – weston Mar 16 '15 at 14:43
  • @weston Use the `object.ReferenceEquals(x, null)`. With `struct` types it is useless, because you can't clearly have two "same" value types (they will always be different instances) – xanatos Mar 16 '15 at 14:44
  • Or you can downcast to `object` :-) (as done in the link) `((object)person1 == null || ((object)person2) == null)` In C# the operators aren't `virtual`, so if you downcast you use the operator of the downcasted class. – xanatos Mar 16 '15 at 14:45
  • The point is, the work must be done in the `==` be that the downcast, or `ReferenceEquals`. If you don't null check the parameters to `==`, you risk calling `null.equals(..)` – weston Mar 16 '15 at 14:46
  • @weston Yes, but A) You downcast to object and use == or B) you use object.ReferenceEquals(x, null). You don't do `x == null` (because you then become recursive and stack overflow) – xanatos Mar 16 '15 at 14:48
  • See example in link: `Person personObj = obj as Person; if (personObj == null)` It does do this. And doesn't cause circular because `==` itself performs a `null` check which it must do. – weston Mar 16 '15 at 14:49
  • So that's `==` in both the `Equals` methods, on variables of type `Person` – weston Mar 16 '15 at 14:50
  • I think you could only get a stackoverflow with an incorrect `==` implementation. i.e. one that does not do `null` checks itself. – weston Mar 16 '15 at 14:54
  • @weston The example given is playing with fire... It is going from `Equal` to `==` to `Equal`. That example is correct, but I'm not even able to comprehend how it works exactly. It is **very** error prone. – xanatos Mar 16 '15 at 14:57
  • Well it's not the same `Equals` method. It goes from `Person.Equals` to `==` to `Object.Equals` in the case of a `null`/non `Person` object. As for playing with fire, it's easy to make a mistake in either approach. I'd hope that all cases are covered by tests in production and would catch SO and other errors. – weston Mar 16 '15 at 15:10