tags:

views:

702

answers:

5

I've defined a C# class with a string member. For all intents an purposes, think of this class as being a subclass of string (except that's not allowed). I'm using it to represent a strongly typed string field that matches a specific format (I've simplified this significantly).

public class field
{
    private readonly string m_field;
    public field(string init_value)
    {
        //Check the syntax for errors
        if (CheckSyntax(init_value))
        {
            m_field = init_value;
        }
        else
        {
            throw new ArgumentOutOfRangeException();
        }
    }

    public override string ToString()
    {
        return m_field;
    }
}

Now, I want to be able to compare this class directly to any other string (object or literal). Therefore, I implemented the following in the class:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }

    return this.m_field == obj.ToString();
}

public override int GetHashCode()
{
    return this.m_field.GetHashCode();
}

public static bool operator ==(field x, Object y)
{
    if ((object)x == null && y == null)
    {
        return true;
    }
    else if ((object)x == null || y == null)
    {
        return false;
    }
    else
    {
        return (x.m_field == y.ToString());
    }
}

public static bool operator !=(field x, Object y)
{
    return !(x == y);
}

Now when I'm writing a unit test, depending on the order that I'm passing in the arguments to Assert.AreEqual, I get different results:

string valid = "Some String";
field target = new field(valid);
Assert.AreEqual(target, valid); // PASSES
Assert.AreEqual(valid, target); // FAILS

I'm assuming this is because in the first assert, it's calling field.Equals() and in the second it's calling String.Equals(). Obviously I'm approaching this from the wrong angle. Can anyone give me some insight?

One other thing. I can't use a struct here (value type) because in my actual case I'm defining all this in a base class and inheriting from it.

+9  A: 

Basically you can't do what you want to - there's no way you can make string recognise your class for equality purposes. You'll never be able to make it reflexive - you'll never be able to make it obey the contract of object.Equals.

I would personally try to redesign it so that you didn't have the validation as part of the type itself - make it part of the relevant properties of the business entities (or whatever they are).

Jon Skeet
+1 Types like these are tricky to use and tricky to have others use consistently. It is better to keep validation and other business logic in the entity itself.
Andrew Hare
Unfortunately I'm using instances of this class as keys in dictionaries and in collections and so forth. I need Equals() and GetHashCode() to behave like value types so they work correctly in those situations.
Scott Whitlock
Override the Equals() and you may want to consider implementing IComparable http://msdn.microsoft.com/en-us/library/system.icomparable.aspxI really wish MS would take operator overloading out of c#.
Chad Grant
@Deviant - thanks. Actually if I could just inherit from String and add my own constructor, I wouldn't have this problem.
Scott Whitlock
@Scott: Yes, you'd still have the same problem. "foo".Equals(instanceOfYourType) still wouldn't return true. Equality and inheritance is a fundamentally tricky problem.
Jon Skeet
@Jon - You are correct that what I was trying to do won't work. (Makes sense why I was posting the question). My intended question is at the bottom of the original post: "Obviously I'm approaching this from the wrong angle. Can anyone give me some insight?" In the real world I can live with those 2 assert statements both passing or both failing, but not one of each. So "foo".Equals(instanceOfField) returning false is fine, as long as instanceOfField.Equals("foo") also returns false. This is why I posted my own answer.
Scott Whitlock
+3  A: 

I would discourage anyone using your field class implicitly as a String, and force this type of usage:

string valid = "Some String";
field target = new field(valid);
Assert.AreEqual(target.toString(), valid); 
Assert.AreEqual(valid, target.toString());
Chris Thornhill
+5  A: 

This is described in detail in Effective Java as Item 8: Obey the general contract when overriding equals.

The equals method implements an equivalence relation.

It is Reflexive, Symmetric, Transitive, Consistent, and for any non-null reference x, x.equals(null) must return false. The example cited to break symmetry is similar to yours.

field class is aware of string class, but the built-in string class is not aware of field. This a one-way interoperability, and should be removed.

eed3si9n
+1 - very clear, thanks!
Scott Whitlock
A: 

Based on everyone's feedback, and my own needs, here's what I'm putting forward as a possible solution (I'm modifying the Equals method as follows):

public override bool Equals(Object obj)
{
    if (obj == null)
    {
        return false;
    }

    field f = obj as field;
    if (f != null)
    {
        return this == f;
    }
    else
    {
        return obj.Equals(this);
    }
}

This seems to allow its correct use in dictionary and collection classes that rely on the Equals and GetHashCode methods for determining if the value already exists.

Also, now these both fail:

string valid = "Some String";
field target = new field(valid);
Assert.AreEqual(target, valid); // FAILS
Assert.AreEqual(valid, target); // FAILS

And these both pass:

string valid = "Some String";
field target = new field(valid);
Assert.AreEqual(target.ToString(), valid); // PASSES
Assert.AreEqual(valid, target.ToString()); // PASSES

And these both pass:

field f1 = new field("Some String");
field f2 = new field("Some String");
Assert.AreEqual(f1, f2); // PASSES
Assert.AreEqual(f2, f1); // PASSES
Scott Whitlock
That is more or less the default Equals operator so not overriding it would give the equivalent functionality. Since your fields represent textual data, you could use "return this.text == f.text" instead. But in this case you need to override GetHashCode to something like return this.text.GetHashCode() which would ensure collection classes behave good.
Mikko Rantanen
Yes, if you look at the original question, it has an override for GetHashCode().
Scott Whitlock
this won't work... the canonical Equals implementation has a check like if this.GetType() != arg.GetType() return false. So doing a "value".Equals( fieldObj ) is bound to fail. No way around it.
Gishu
@Gishu - yes, that is intended. As long as "value".Equals(fieldObj) and fieldObj.Equals("value") both return the same thing, that's fine. Then I can fall back on "value".Equals(fieldObj.ToString()). No problem.
Scott Whitlock
A: 

This is String#Equals

public override bool Equals(object obj)
{
    string strB = obj as string;
    if ((strB == null) && (this != null))
    {
        return false;
    }
    return EqualsHelper(this, strB);
}

Supplying an argument other than a String to String#Equals is going to return false. I'd suggest a 'rethink' to get around this.

Gishu
On a side note, Why the (this != null) - Isn't this guaranteed to be non-null inside an instance method ?
Gishu