Our requirements from an index-type are fairly minimal…
public interface IIndex { int Value { get; } }
… and this would be enough for a number of scenarios, although not one particular scenario that I wanted: being able to construct a typed-index from an int
. This operation is useful for being able to expose existing properties of an array as their typed equivalent, in particular I wanted to expose the length of the array in each dimension.
public interface IIndex<TIndex> : IIndex where TIndex : struct, IIndex<TIndex> { TIndex WithValue(int value); }
The above is then used in conjunction with the fact that T : struct
implies T : new()
to essentially gain a T : new(int)
constraint by using var t = new T().WithValue(i)
. I could call this a tweak on the prototype-pattern but at the end of the day it’s a bit of a hack around the type constraints I’m able to achieve with generics.
Due to index types needing to be structs we can’t use inheritance, making implementing an index type a rather tedious and unpleasant copy-paste affair. While this obviously has plenty of disadvantages, it has the benefit that each index type is free to enabled – or not – methods and operations as it sees fit, for example IComparable<>, IEquatable<>, operator overloads, &c. No more than five or six index types will be created, with some having the requirement to be comparable (e.g. yearA
< yearB
) and others with the requirement to explicitly not be comparable (e.g. simulation
– each is fully independent of all others) so I consider the trade-offs being made here to be acceptable.
Implementing the wrappers for arrays was also a rather tedious job as each rank requires a separate implementation with the correct number of type parameters.
public class StrongArray<TValue, TIndex0, TIndex1> : StrongArray where TIndex0 : IIndex<TIndex0> where TIndex1 : IIndex<TIndex1> { private readonly TValue[,] array; public StrongArray(TIndex0 length0, TIndex1 length1) : this(new TValue[length0.Value, length1.Value]) { } public StrongArray(TValue[,] array) : base(array) { this.array = array; } public TValue this[TIndex0 index0, TIndex1 index1] { get { return array[index0.Value, index1.Value]; } set { array[index0.Value, index1.Value] = value; } } public TIndex0 Length0 { get { return new TIndex0().WithValue(array.GetLength(0)); } } public TIndex1 Length1 { get { return new TIndex1().WithValue(array.GetLength(1)); } } public TValue[,] Array { get { return array; } } }
And, no, I couldn’t think of a better name at the time!
Was it worth it?
Definitely.
For about a day an a half of coding, what Code Metrics tells me is 125 lines of code – with the biggest contributor being a class of helper methods such as Enumerable.Range equivalents – and a very modest performance penalty, this thin abstraction over arrays has had a significant impact on how much information is conveyed by our code and has helped to reduce the amount of time and effort required to understand a piece of code.