C# Grief With Overloaded Operators

Save/Share Google Yahoo! Digg It Reddit del.icio.us
My Zimbio

Here’s a nasty bug that crept into one of my team’s projects recently. When will equivalent C# objects fail to compare as equal? The answer is “when operator== doesn’t work as expected.”

Take a look at this method. It’s similar to a method I ran across that was intended to provide a generic means of performing some behavior based on the equivalence of two values:

static public void DoSomeWork(object obj1, object obj2)

{

Console.WriteLine(“obj1 = ” + obj1 + “; obj2 = ” + obj2 + “;”);

if (obj1 == obj2)

{

Console.WriteLine(“Objects are equal”);

// do some important work

}

else

{

Console.WriteLine(“Objects are NOT equal”);

// do some different important work

}

}

Note the use of operator== being applied to two parameters of type object. The idea here is we can pass objects of any type: int, string, or some complex type that implements Equals(). No matter what we pass, we want the code to branch based on the equivalence of the arguments.

Now, as a former Java developer, I never would have coded it this way. In Java, “obj1 == obj2” is always a reference comparison. It evaluates to true if, and only if, both arguments refer to precisely the same object instance. To compare objects for equivalence, Java programmers explictly test “obj1.equals(obj2)“.

In my C++ days, I learned that operator overloading is a risky endeavor that generally should be avoided. Given a flat class hierarchy, you’re probably safe. But, sometimes the operator functionality that gets invoked on a derived object cast as a base type is non-intuitive.

Like in this C# example.

Look what happens when we pass equivalent string instances to the above method.

static void Main()

{

// build the first string

string str1 = “foobar”;

// make the second string equivalent

// use concatenation to prevent compiler optimization

string str2 = String.Concat(“foo”, “bar”);

DoSomeWork(str1, str2);

}

Note how the second string is built using concatenation. This is necessary to prevent the compiler from using the same instance of the string literal on both assignments (this would invalidate our test).

With our string instances both equivalent to “foobar”, we call our test method and get:

obj1 = foobar; obj2 = foobar;
Objects are NOT equal

What’s up with that? Every C# developer knows that "foobar" == "foobar" will always evaluate to true since, unlike Java, C# is “smart” enough to invoke the Equals() method when operator== is called on two strings.

The problem lies in the fact that operator== is a static method that must be resolved at compile time, not a polymorphing method selected at runtime.

If obj1 and obj2 were explicity cast as string, the code would work as expected. The static method operator==( string str1, string str2 ) method is implemented to invoke Equals() for you. The Equals() method on type object is declared virtual, so it is a polymorphic method.

However, in our DoSomeWork() example, the parameters are cast as object. So, the static operator==( object obj1, object obj2 ) method is selected at compile time. And that implementation of the overloaded == operator only performs an object reference comparison. The Equals() method is never invoked!

A similar problem occurs if you try to pass a value type. The value types will get boxed before DoSomeWork() gets called. Hence, even “2 == 2” will evaluate to false.

The solution? Rewrite DoSomeWork() to invoke Equals():

static public void DoSomeWork(object obj1, object obj2)

{

Console.WriteLine(“obj1 = ” + obj1 + “; obj2 = ” + obj2 + “;”);

if (obj1.Equals(obj2))

{

Console.WriteLine(“Objects are equal”);

// do some important work

}

else

{

Console.WriteLine(“Objects are NOT equal”);

// do some different important work

}

}

Now, we’ll get the comparison behavior we wanted.

Save/Share Google Yahoo! Add to Technorati Favorites Digg It Reddit
del.icio.us My Zimbio

Leave a Reply