18

I'm writing a wrapper for XML elements that allows a developer to easily parse attributes from the XML. The wrapper has no state other than the object being wrapped.

I am considering the following implementation (simplified for this example) which includes an overload for the == operator.

class XmlWrapper
{
    protected readonly XElement _element;

    public XmlWrapper(XElement element)
    {
        _element = element;
    }

    public string NameAttribute
    {
        get
        {
            //Get the value of the name attribute
        }
        set
        {
            //Set the value of the name attribute
        }
    }

    public override bool Equals(object other)
    {
        var o = other as XmlWrapper;
        if (o == null) return false;
        return _element.Equals(o._element);
    }

    public override int GetHashCode()
    {
        return _element.GetHashCode();
    }

    static public bool operator == (XmlWrapper lhs, XmlWrapper rhs)
    {
        if (ReferenceEquals(lhs, null) && ReferenceEquals(rhs, null)) return true;
        if (ReferenceEquals(lhs, null) || ReferenceEquals(rhs, null)) return false;

        return lhs._element == rhs._element;
    }

    static public bool operator != (XmlWrapper lhs, XmlWrapper rhs)
    {
        return !(lhs == rhs);
    }
}

As I understand idiomatic c#, the == operator is for reference equality while the Equals() method is for value equality. But in this case, the "value" is just a reference to the object being wrapped. So I am not clear what is conventional or idiomatic for c#.

For example, in this code...

var underlyingElement = new XElement("Foo");
var a = new XmlWrapper(underlyingElement);
var b = new XmlWrapper(underlyingElement);

a.NameAttribute = "Hello";
b.NameAttribute = "World";

if (a == b)
{
    Console.WriteLine("The wrappers a and b are the same.");
}

....should the program output "The wrappers a and b are the same" ? Or would that be odd, i.e. violate the principal of least astonishment?

5
  • For all the times I overrode Equals I never overrode == (but never the other way around). Is lazy idiomatic? If I get different behavior without an explicit cast that violates least astonishment.
    – radarbob
    Sep 10, 2019 at 22:49
  • The answer to this depends on what the NameAttribute does - modifies the underlying element? Is an additional piece of data? The meaning of the example code (and whether it should be considered equal) changes depending on that, so I think you need to fill it in.
    – Errorsatz
    Sep 10, 2019 at 23:38
  • @Errorsatz I apologize, but I wanted to keep the example concise, and I assumed it was clear that it would modify the wrapped element (specifically by modifying the XML attribute named "name.") But the specifics barely matter-- the point is that the wrapper allows read/write access to the wrapped element but contains no state of its own.
    – John Wu
    Sep 10, 2019 at 23:40
  • 4
    Well, in this case they matter - it means that the "Hello" and "World" assignment is misleading, because the latter will overwrite the former. Which means I agree with Martin's answer that the two can be considered equal. If the NameAttribute were actually different between them, I wouldn't consider them equal.
    – Errorsatz
    Sep 10, 2019 at 23:56
  • 2
    "As I understand idiomatic c#, the == operator is for reference equality while the Equals() method is for value equality." Is it though? Most of the times I've seen == overloaded is for value equality. The most important example being System.String. Sep 11, 2019 at 17:00

3 Answers 3

16

Since the reference to the wrapped XElement is immutable, there is no externally observable difference between two instances of XmlWrapper that wrap the same element, so it makes sense to overload == to reflect this fact.

Client code almost always cares about logical equality (which, by default, is implemented using reference equality for reference types). The fact that there are two instances on the heap is an implementation detail that clients shouldn't care about (and those that do will use Object.ReferenceEquals directly).

9

If you think it makes the most sense

The question and answer is a matter of developer expectation, this is not a technical requirement.

IF you consider a wrapper to not have an identity and have it be defined purely by its contents, then the answer to your question is yes.

But this is a recurring problem. Should two wrappers exhibit equality when they wrap different objects but with both objects having the exact same contents?

The answer repeats itself. IF the content objects have no personal identity and instead are purely defined by their content, then the content objects are effectively wrappers which will exhibit equality. If you then wrap the content objects in another wrapper, that (additional) wrapper should then also exhibit equality.

It's turtles all the way down.


General tip

Whenever you deviate from default behavior, it should be explicitly documented. As a developer, I expect that two reference types will not exhibit equality even if their contents are equal. If you change that behavior, I would suggest you clearly document it so that all developers are aware of this atypical behavior.


As I understand idiomatic c#, the == operator is for reference equality while the Equals() method is for value equality.

That is its default behavior, but this is not an immovable rule. It's a matter of convention, but conventions can be changed where justifiable.

string is a great example here, as == is also a value equality check (even when there is no string interning!). Why? Simply put: because having strings behave like value objects feels more intuitive to most developers.

If your codebase (or your developers' lives) can be notably simplified by having your wrappers exhibit value equality across the board, go for it (but document it).

If you never require reference equality checks (or they are rendered useless by your business domain), then there's no point to keeping a reference equality check around. It's better to then replace it with a value equality check so as to prevent developer error.
However, do realize that should you need reference equality checks later down the line, reimplementing it may take a notable effort.

3
  • I'm curious why you expect that reference types won't define content equality. Most types don't define equality simply because it's not essential to their domain, not because they want reference equality.
    – casablanca
    Sep 11, 2019 at 8:57
  • 3
    @casablanca: I think you're interpreting a different definition of "expect" (i.e. requirement versus assumption). Without documentation, I expect (i.e. assume) that == checks for reference equality as this is the default behavior. However, if == actually checks for value equality, I expect (i.e. require) that this is documented explicitly. I'm curious why you expect that reference types won't define content equality. They don't define it by default, but that doesn't mean it can't be done. I never said it can't (or shouldn't) be done, I simply don't expect (i.e. assume) it by default.
    – Flater
    Sep 11, 2019 at 9:20
  • I see what you mean, thanks for clarifying.
    – casablanca
    Sep 12, 2019 at 5:17
2

You are basically comparing strings so I would be astonished if two wrappers containing the same XML content would not be considered equal, be it checked using Equals or ==.

The idiomatic rule may make sense for reference type objects in general but strings are special in an idiomatic sense, you are supposed to treat and regard them as values although technically they are reference types.

Your Wrapper postfix adds confusion though. It basically says "not an XML element". So should I treat it as a reference type after all? Semantically this would not make sense. I would be less confused if the class were named XmlContent. This would signal we care about content, not technical implementation details.

2
  • Where would you put the limit between "an object built from a string" and "basically a string" ? It seems quite an ambiguous definition from the outside API perspective. Does the user of the API actually have to guess the internals to guess the behavior ?
    – Diane M
    Sep 10, 2019 at 23:11
  • @Arthur Semantics of the class/object should provide the clue. And sometimes it is not all that obvious, hence this question. For real value types it will be obvious to anyone. For real strings also. The type should ultimately tell whether a comparison should involve content or object identity. Sep 10, 2019 at 23:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.