Practical and theoretical differences between a C# int and a struct containing just an int

Viewed 71

I'm considering various advice I've seen about a solution for C#'s lack of typedef, that is to create a struct with one member of the type you want to "typedef". For example:

struct SpecialIndex {
  private int value;
  public SpecialIndex(int value) {
    this.value = value;
  }

  // implement necessary operations: add, IEquatable, increment, etc.
  //...
}

List<SpecialThing> specialThings = new List<SpecialThing> { ...... };

SpecialThing GetSpecialThingByIndex(SpecialIndex idx) {
  return specialThings[idx.value];
}

void Main() {
  SpecialIndex myThingIndex = new SpecialIndex(3);
  int someInt = 5 + 12;
  
  SpecialThing thing1 = GetSpecialThingByIndex(myThingIndex); // nice
  SpecialThing thing2 = GetSpecialThingByIndex(someInt); // woah sonny, what makes you think this is an index?
}

Putting aside the potential awkwardness of properly implementing such a struct and assuming its internals were up-to-snuff, what are the both the technical and usability gotchas of such an approach?

Thanks

1 Answers

I have used this struct wrapper approach in a couple of places and always got mixed results. I'm afraid that this is always use-case specific and that you will have to try and see how it works for you. What follows is my mileage.

If you have for example Dictionary<string, string>, create a couple of types and get Dictionary<PropertyName, PropertyValue>, then that works excellent for readability and usability, especially if you do some LINQ queries with such types. So this is a use-case I can recommend and a place where it worked for me: to provide a more precise name for a set of values that you don't want to mix up, especially if the underlying type is identical.

It has, however, only limited value for correctness as the wrapped values usually do come from outside of your system (otherwise you would use enum, inheritance or redesigned your API, right?). So in the end, you only move the value validation from the actual index access to the constructor of the wrapper type. You might think that you are getting something from the type system, but ultimately, your wrapper type still accepts int and the consumer of your API has to read the docs on what the constraints of this int are. But instead of visiting the API they want to use, they now need to navigate one place further and memorize additional type. For this reason I have so far had negative experience in defining special index types this way.

Just a nitpick, a typedef would not resolve your issue as it creates a type alias similar to C#'s using - i.e. no new type is created, the types are still compatible, only have different names. What you're looking for is newtype as in Haskell.

Related