JonnyRaa
JonnyRaa

Reputation: 8038

How to check whether generic objects are equal that works for strings

I ran into some very wierd behaviour today whilst refactoring some code.

I had some code that looked something like this:

    private AType Blah
    {
        get
        {
            return (from something in AList
                    where _x == null || something.x == _x
                    where _y == null || something.y == _y
                    where _z == null || something.z.IsSameAs(_z)
                    select something).Single();
        }
    }

I've anonomised the type and variable names as they aren't important to the question.

The type of _x and something.x was string and the _y and something.y was a reference type. Likewise _z and something.z were a reference type with a value comparison.

I thought I could do something like this:

    public AType Blah
    {
        get { return AList.Single(something => DetailsMatch(something.x, something.y, something.z)); }
    }

    private bool DetailsMatch(string x, AnotherType y, AFurtherType z)
    {
        return NullOrCheck(_x, x) &&
               NullOrCheck(_y, y) &&
               NullOrCheck(_z, z.IsSameAs);
    }

    private bool NullOrCheck<T>(T value, T expected) where T : class
    {
        return NullOrCheck(value, v => v == expected);
    }

    private static bool NullOrCheck<T>(T value, Func<T,bool> check) where T : class
    {
        return value == null || check(value);
    }

This all seemed to make sense but to my surprise some tests started failing. It turned out that the identical strings (the example was "1A04" and "1A04") were no longer being considered equal using the == operator.

Having looked at the following Can't operator == be applied to generic types in C#? it seems likely that the strings were being compared on reference equality instead of in the normal manner.

Is there a safe way to do this in c# or should using == in a generic method be considered dangerous for the above reason?

Just to confirm that this was the problem my fix consisted of inlining the offending methods in the string case resulting in:

    private bool DetailsMatch(string x, AnotherType y, AFurtherType z)
    {
        return (_x == null || _x == x) &&
               NullOrCheck(_y, y) &&
               NullOrCheck(_z, z.IsSameAs);
    }

Hey presto - everything works and the tests pass again

Upvotes: 1

Views: 178

Answers (1)

Lee
Lee

Reputation: 144136

You can use Object.Equals:

return NullOrCheck(value, v => object.Equals(v, expected));

The string class overloads the static == operator to compare its two string arguments for equality i.e.

string first = "abc";
string second = "abc";

bool eq = first == second;

The call to == will use the overloaded == for strings, since the static type of first and second are both string.

However, in

object first = "abc";
object second = "abc";
bool eq = first == second;

The == operator used will be the one defined by object since the static type of first and second is object. Note that in this case, due to string interning, first and second will actually contain a reference to the same string, however this is not the case in general.

In a generic method, == will resolve to the static == defined for object instead of the more specific version defined for string. Since == is a simple reference equality check for objects, this behaves differently.

The Equals method is virtual and can be overridden to specialise equality checks for custom types. Therefore in

object first = "abc";
object second = "abc";
bool eq = first.Equals(second);

The string.Equals method will be called, which will check the strings have the same values, instead of just the same reference.

The static object.Equals method uses the virtual Equals instance method, so it will also check the strings have the same value instead of just pointing to the same string instance. The static object.Equals also checks its argument for null, so is safer than calling objA.Equals(objB) directly.

Upvotes: 4

Related Questions