2

I have several classes with id property of the same type int?:

public class Person {
  public int? id { get; set; }
}

public class Project {
  public int? id { get; set; }
}

// etc...

When writing code it happened that I compared semantically wrong types: if (person.id == project.id), and of course there was no warning until I found the bug.

How could I create some kind of underlying type enforcement, or even better, a compiler warning, or something like that, that warns me not everything looks o.k.?

I can think of creating an Equals(Person p) { return p.id == this.id } but I'd prefer some other mechanism that could be used more 'freely'.

Spikolynn
  • 4,067
  • 2
  • 37
  • 44
  • This c++ question seems to go in the same direction, but I fail to see how I could use the answer: https://stackoverflow.com/questions/39412785/compiler-enforced-semantic-types – Spikolynn Nov 25 '19 at 09:45
  • 1
    Instead of using an `int?` as the ID type, maybe use a custom wrapper type for it? Ideally, it should have a generic parameter that says what type of id it is. – Sweeper Nov 25 '19 at 09:46
  • 1
    Personally, making a structure for this seems like overkill, this should be picked up in unit tests – TheGeneral Nov 25 '19 at 09:51
  • 2
    What you can do before all is create a base class with id property only and derive from it in all the types that should have that id. but the question of if you are comparing right objects is just too demandy of the language :) how should the runtime know that you are comparing wrong IDs? how do you define wrong for the compiler? I simply see no point in persuing this purpose since they are both of the same type as in int? and should behave like one. – Siavash Rostami Nov 25 '19 at 09:54
  • 1
    What about operator overloading here? Can it be considered as a possible solution here? – Pavel Anikhouski Nov 25 '19 at 09:56
  • Unit tests or IDE warnings would be a preferred solution. Though I cannot see how I could do that. Something involving reflection, probably? – Spikolynn Nov 25 '19 at 09:57
  • but if you have a requirement in the form of : if two objects have equal IDs then they are equal then you can overload the `eqauls` method or `==` operator for specific types. – Siavash Rostami Nov 25 '19 at 09:57
  • 1
    @Pavel Overriding == would also work, yes, thank you – Spikolynn Nov 25 '19 at 10:02

2 Answers2

2

You need to override Equals and GetHashCode to be able to compare objects directly.

Try like this:

public sealed class Person : IEquatable<Person>
{
    private readonly int? _id;

    public int? Id { get { return _id; } }

    public Person(int? id)
    {
        _id = id;
    }

    public override bool Equals(object obj)
    {
        if (obj is Person)
            return Equals((Person)obj);
        return false;
    }

    public bool Equals(Person obj)
    {
        if (obj == null) return false;
        if (!EqualityComparer<int?>.Default.Equals(_id, obj._id)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<int?>.Default.GetHashCode(_id);
        return hash;
    }

    public override string ToString()
    {
        return String.Format("{{ Id = {0} }}", _id);
    }

    public static bool operator ==(Person left, Person right)
    {
        if (object.ReferenceEquals(left, null))
        {
            return object.ReferenceEquals(right, null);
        }

        return left.Equals(right);
    }

    public static bool operator !=(Person left, Person right)
    {
        return !(left == right);
    }
}

public sealed class Project : IEquatable<Project>
{
    private readonly int? _id;

    public int? Id { get { return _id; } }

    public Project(int? id)
    {
        _id = id;
    }

    public override bool Equals(object obj)
    {
        if (obj is Project)
            return Equals((Project)obj);
        return false;
    }

    public bool Equals(Project obj)
    {
        if (obj == null) return false;
        if (!EqualityComparer<int?>.Default.Equals(_id, obj._id)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<int?>.Default.GetHashCode(_id);
        return hash;
    }

    public override string ToString()
    {
        return String.Format("{{ Id = {0} }}", _id);
    }

    public static bool operator ==(Project left, Project right)
    {
        if (object.ReferenceEquals(left, null))
        {
            return object.ReferenceEquals(right, null);
        }

        return left.Equals(right);
    }

    public static bool operator !=(Project left, Project right)
    {
        return !(left == right);
    }
}

I also implemented IEquatable<Person> and == and != for good measure.

Now you can write person1 == this if this is a Person, but you would have a compiler error if this were a Project.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Out of curiosity, what is the added value of setting the `hash` to `0` and then doing `hash ^= EqualityComparer.Default.GetHashCode(_id);`? (as far as I understand this should be exactly the same as the result of `EqualityComparer.Default.GetHashCode(_id);` but wondering if I miss something) –  Nov 25 '19 at 10:14
  • 1
    @Knoop - I have a script for generating read-only classes. I just put in the name of the class and all of the properties and it builds them for me. So I need one line to declare the variable and then there is a line for each property. It's just for simplicity. – Enigmativity Nov 25 '19 at 10:17
  • Fair enough, that makes sense. Thanks for the reply –  Nov 25 '19 at 10:19
2

This is what tests are for. This is why you should write tests. Tests should pick up on these kind of errors.

But if you really want to go overkill, create a custom struct to store your IDs:

public struct Id<T> {
    public int? ID { get; }

    public static implicit operator Id<T>(int id) {
        return new Id<T>(id);
    }

    public Id(int? id) { ID = id; }

    public static bool operator ==(Id<T> lhs, Id<T> rhs) {
        return lhs.ID == rhs.ID;
    }
    public static bool operator !=(Id<T> lhs, Id<T> rhs) {
        return lhs.ID != rhs.ID;
    }
}

// usage:

public class Person {
  public Id<Person> Id { get; set; }
}

public class Project {
  public Id<Project> Id { get; set; }
}

Whenever you try to compare Person.Id with Project.Id, the compiler will give you an error because you are comparing Id<Project> and Id<Person>.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    I like the idea of creating a `Id` struct, but doesn't using generics mean that we are out of the frying pan and into the fire with the OP's issue? I mean what if I defined this `public class Project { public Id Id { get; set; } }`? – Enigmativity Nov 25 '19 at 10:01
  • 1
    @Enigmativity Indeed, but you generally declare IDs far less often than you compare them, so the frequency of making a mistake is greatly reduced. – Sweeper Nov 25 '19 at 10:03
  • @Sweeper - Fair enough. – Enigmativity Nov 25 '19 at 10:18
  • @Sweeper I like this idea too, though I would love to see also in what direction you would write tests for this case – Spikolynn Nov 25 '19 at 10:59