1

Note: This question is nearly identical to this one. But this one is about C#, not Java.

In Ada, it is possible to create incompatible equivalent numeric types:

type Integer_1 is range 1 .. 10;
type Integer_2 is range 1 .. 10;
A : Integer_1 := 8;
B : Integer_2 := A; -- illegal!

This prevents accidental logical errors such as adding a temperature to a distance.

Is it possible to do something similar in C#? E.g.

class BookId : int {}
class Book
{
    BookId Id;
}

class PageId : int {}
class Page
{
    PageId Id;
}

class Word
{
    BookId BookId;
    PageId PageId;
    string text;
}

var book = new Book { Id = 1 };
var page = new Page { Id = 1 };
var book = new Word
{
   BookId = book.Id,  // Ok
   PageId = book.Id,  // Illegal!
   string = "eratosthenes"
};
Holt
  • 36,600
  • 7
  • 92
  • 139
Avi Chapman
  • 319
  • 1
  • 8

2 Answers2

1

Yes, you can create types that behave like numeric values but can't be assigned to each other. You can't derive from numeric type, but wrapping one into a struct would be comparable efficient (if that's a concern) or you can add more info (like units). You may be even create generic type if you don't need cross-type operations.

You can see Complex type for full set of operations and interfaces that make type behave very close to regular numbers (including plenty of conversions back and forth as needed).

Some basic class:

class Distance
{
    float d;
    public Distance(float d)
    {
        this.d = d;
    }

    public static Distance operator+(Distance op1, Distance op2)
    {
        return new Distance(op1.d + op2.d);
    }

    // ==, !=, Equals and GetHashCode are not required but if you 
    // need one (i.e. for comparison you need ==, to use values of this 
    // type in Dictionaries you need GetHashCode)
    // you have to implement all 
    public static bool operator == (Distance op1, Distance op2)
    {
        return op1.d == op2.d;
    }
    public static bool operator !=(Distance op1, Distance op2)
    {
        return op1.d != op2.d;
    }

    public override bool Equals(object obj)
    {
        return (object)this == obj || ((obj is Distance) && (obj as Distance)==this);
    }
    public override int GetHashCode()
    {
        return d.GetHashCode();
    }

    // Some basic ToString so we can print it in Console/use in 
    // String.Format calls
    public override string ToString()
    {
        return $"{d} M";
    }
}

Which lets you add values of the same type but will fail to add any other type:

Console.WriteLine(new Distance(1) + new Distance(2)); // "3 M"
// Console.WriteLine(new Distance(1) + 2); // fails to compile

Picking between class and struct for such sample is mostly personal preference, for real usage make sure to know difference between value and reference type before picking one and decide what works for you (struct is likely better for numbers).

More information: Units of measure in C# - almost - even if you don't go all the way it shows how to make generic numeric type so you can easily create many types without much code (UnitDouble<T> in that post), Arithmetic operator overloading for a generic class in C# - discusses issues you face if you want to go other way and support varying base numeric types (like Distance<float> and Distance<int>).

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
0

It turns out my needs were a bit more simple than I thought. What I needed was a unique ID that could not be confused with another unique ID. In the end, I went with a template wrapper for an int.

class Id<T> {
  private int id;
  public Id(int id) { this.id = id; }
  public static implicit operator ID<T>(int value) { return new ID<T>(value); }
  public static implicit operator int(ID<T> value) { return value?.id ?? 0; }
  public static implicit operator int?(ID<T> value) { return value?.id; }  
  public static implicit operator ID<T>(int? value)
  {
    if (value == null) { return null; }
    return new ID<T>(value.Value);
  }
  public override string ToString() { return id.ToString(); }
}

class Book { Id<Book> Id; }
class Page { Id<Page> Id; }

Book.Id cannot be assigned to Page.Id, but either can go back and forth with ints.

I realise now that I've seen this pattern before somewhere, so I guess it's not that original...

Avi Chapman
  • 319
  • 1
  • 8