6

Suppose I have two classes that implement the same interface, as in the example in C# below:

public interface ICommonInterface
{
    string Text { get; }
    int Count { get; }
}

public class ImplementationA: ICommonInterface
{
    // Implementation A
}

public class ImplementationB: ICommonInterface
{
    // Implementation B
}

I want to define equality for implementations of ITargetOccurence as ITargetOccurence.Text and ITargetOccurence.Count being both equal.

What is a good way of defining such equality, and why?


Possible Solutions I Have Considered:


1) Implementing a Static Method Helper For Equality

I could make equality of interfaces something that can be accessed via a static helper class, such as:

public static class CommonInterfaceHelper
{
    public static bool AreEqual(ICommonInterface A, ICommonInterface B)
    {
        // ...
    }
}

This does not make use of any equality pattern in the language, though.


2) Overriding Equality Operator In The Classes

I could do something similar to the implementation suggested in this question: https://stackoverflow.com/questions/22101703/overriding-equals-in-c-sharp-interface-implementation

public class ImplementationA: ICommonInterface
{
    public override bool Equals(object obj)
    {
       ICommonInterface other = obj as ICommonInterface;
       // ...
    }
}

// Same thing for ImplementationB

I believe that equality returning true for instances of another class is counter intuitive, though.


3) Changing ICommonInterface to a struct

Instead of making ICommonInterface an interface, it could perhaps be a struct that has a better defined equality. The resulting implementation could be something like this:

public struct CommonEnum
{
    string Text;
    int Count;
}

public class ImplementationA
{
    // Implementation A
    public static implicit operator CommonEnum(ImplementationA imp)
    {
        CommonEnum commonEnum = new CommonEnum();
        commonEnum.Text = imp.Text;
        commonEnum.Count = imp.Count;

    };
}

// Same thing for ImplementationB

This unties the ImplementationA and ImplementationB, though.


4) Argue that interfaces (ICommonInterface) should not have a equality constraint

Interfaces are just specifications on what methods a class should have. In that case, one could argue that ICommonInterface should not have a "equality constraint" and that one should check each method individually if comparing equality.


5) Make Equality a Method Defined by the Interface

With the same reasoning as (4) one could argue that if equality was to be expected, the Interface should define:

public interface ICommonInterface
{
    string Text { get; }
    int Count { get; }

    bool IsEqual(ICommonInterface other);
}

This also does not make use of the equality patterns the language provides, though.


Is there any standard way of defining this equality, by following a given set of good practices or principles?

For clarification regarding acceptance criteria for answers, I believe that an answer to this question should be either:

  • An argument that this is an "opinion based" subject and that there are no set of good practices or principles that indicate one of the given approaches is preferred.
  • An argument that one of the approaches is preferred by following a given set of good practices or principles, with some links or explanation of what principles were used.
3
  • 3
    Just implement IEquatable<T> for your concrete implementations (i.e. your classes). See docs.microsoft.com/en-us/dotnet/api/… Commented Nov 18, 2019 at 17:00
  • 1
    also, a shared abstract base class seems prudent if the value equality is necessary.
    – Telastyn
    Commented Nov 18, 2019 at 17:03
  • Also, don't create mutable structs. That's an antipattern in .Net. Commented Nov 19, 2019 at 9:03

1 Answer 1

10

There is no single answer to your question: It strongly depends on what you are modelling/abstracting. In short, most of your proposed solutions are conditionally valid! Some comments with respect to your proposed solutions

  • Option 1: Your idea behind this proposition is, probably, to allow a custom equality to be defined, which is irrespective of the implementation. This is OK, but it's not the best you could do. A static equality method does not help you that much because it is not "composable" (so to speak). Prefer an IEqualityComparer. Why is this more useful? Because you can easily "plug" it into a Dictionary or HashSet and now, your equality method actually does something without the need for further "adaptations". In short, it is easier to re-use an EqualityComparer than a static method (classes are higher-level abstractions than methods).

  • Option 2: This is almost the most reasonable case, because in the vast majority of cases, equality is an implementation detail. However, you don't cast the object to the interface. You cannot decide for another type, only your own type. If you want to be "fair" (in the OOP sense), Equals for a Type should only matter to the same Type. Otherwise, you are breaking the least astonishment principle, but also, your entire model (you make decisions for other types inside your type). This is going to rob you of flexibility and maintainability if you expect more implementations of the interface. Someone decides to implement the interface and, suddenly, finds out his instances are "replaceable" by instances of some other type. In short, your Equals method could end up being invoked in places you might forget.

Notably, the Remarks section of that last link states:

The Default property checks whether type T implements the System.IEquatable interface and, if so, returns an EqualityComparer that uses that implementation. Otherwise, it returns an EqualityComparer that uses the overrides of Object.Equals and Object.GetHashCode provided by T.

  • Option 3: This is a rather extreme measure just to control equality. You are probably hitting a small nail with a large hammer there, all the more so if the operator is implicit. Instead of commenting on why this is not a truly good idea overall, let me remind you that, apart from how your solutions are designed, you should also think about how they are going to be used. This option requires someone to know the additional detail that, when they want to check for equality, they have to create two CommonEnum objects and compare them instead. This is something that needs more explaining than the simple self-evident Equals method.

  • Options 4 & 5: I can not advise because I don't know what these interfaces actually are. For example, if I am designing interfaces for coordinates (for some reason), I could argue that points must have some equality because it is part of their definition. However, there are still better options than this, so equality should probably not be part of the interface because it expands it for no good reason. I mean, you may end up not needing to use it at all, so why define it up front. There is no thing such as "best practices" here because you are designing both the model, and the subtleties. In any case, if you decide to force a method, just make your interface extend IEquatable<T> instead. This makes it more versatile when it comes to the patterns of the language you refer to.

The TL;DR version is the simple suggestion that, using external EqualityComparers, there is virtually no scenario you cannot serve, let alone some of the most important ones (e.g. HashSets and Dictionarys) directly out-of-the-box. Pair that with the fact that you don't have to even touch your interfaces and classes, and there you have it. Versatile tailor-made equality, without touching the definitions. Thinking that IEquatable might make your objects less intuitive (because it is an additional layer of information that a developer-reader has to process, during which a lot of "why"s can occur), I am hard-pressed to conclude that IEqualityComparer would be the least-astonishing practice, ergo the typically most useful option.

P.S.: As a bonus, I have a real-world scenario for you, where IEquatable is the option to go with! You have built a huge codebase and, thus far, you have gotten by with the default equality, which is based on reference equality. That means that EqualityComparers you simply did not bother too much with. So far so good... Now you have to change an immutable object, by creating another immutable object in-place (with one different property) and replace the old one. But everywhere in your vast mess of code, wherever you had tied that object to various things (i.e. used it as a key), you are going to lose all these relations and you cannot really bother to update all places where your object was used as a key, in order to update the key with the new immutable instance. This will necessitate too much searching.

In this scenario, you realize that, while it's very late, it's not too late. You can now implement IEquatable properly, and suddenly, your two instances of the immutable object actually end up creating the same key (unless, of course, you changed a property that defines equality, which you didn't). Now you can replace the immutable instance and all your Dictionaries and HashSets, etc., will not break the key-value relations!

Your Answer

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

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