[Note: This question has attracted solutions with low runtime performance, so it's worth emphasizing here that performance is key, in addition to the type safety. See the note at the end for more performance info.]
I am using a few different instances of List<int>
in a fairly complex algorithm.
Some of these lists contain indexes into each other, i.e. they provide a level of indirection.
I've fixed a few bugs caused by the wrong indexer being used while accessing a list. Because all the lists are the same type, i.e. List<int>
, the compiler doesn't provide any type safety at all. For example:
// The below statement is wrong - it should be list1[list2[x]],
// as x is an index into list2, not list1.
// list2 returns indexes into list1.
// But the compiler is oblivious to this.
//
var f = list1[x];
So, I started thinking that it may be possible to add a degree of type safety by using strongly typed indexes into each list that just wrap an integer:
/// An index into the first list
struct Index1
{
public int Value { get; set; }
}
/// An index into the second list
struct Index2
{
public int Value { get; set; }
}
Then, declaring variables of the correct index type will catch a class of bugs at compile time. (This is not foolproof, that's not what I'm after - better is enough.)
Unfortunately a generic List<T>
does not offer a way to use a custom index type - the indexer is always an int.
Is there another way to accomplish what I'm trying to do? A custom collection comes to mind - I don't mind the effort, which will likely pay for itself. But I can't think of one that can be used as shown. (I could create a separate collection type for each indexer, of course - but I'd like to use a single new collection type if possible - because otherwise code duplication starts to become an issue.)
Performance:: The algorithm was rewritten to use lists for performance. We're even considering using arrays since there is one less bounds check. So any proposed solution should have excellent runtime performance - comparable at least to List<T>
.
Therefore the struct (or any other technique) should ideally be optimized out after the compile-time type-safety checks.
Use Cases: The use cases are:
- Accessing a random element from a list via a type safe indexer.
- A for loop over a list, also using a type safe indexer.
- Using foreach over a list. List optimizes its GetEnumerator() to return a struct and so avoid allocations. I'd like to preserve that.