In my book, I present an example of a Range<T>
class do demonstrate iterator blocks. The range allows you to iterate over each element within it in the obvious fashion. There’s an abstract base class, and then a couple of concrete classes derived from that – enough to show the pattern. The base class is abstract because there’s a single abstract method, GetNextValue
, which is required to take a current value and return the next one in the sequence. How this occurs depends on the types involved – in the case of a range of DateTime
elements, it will add a TimeSpan
each time, for instance, whereas an Int32Range
will just add an int
. Here’s a class diagram of how it looks:
The requirements for the code in the book were very simplistic, in order to be able to present all the code on the printed page. However, I wanted to expand this in MiscUtil and “do the job properly”. In particular, I wanted to be able to:
- Reverse a range
- Make a range exclusive (at the “far end” – a half-open interval)
- Make an exclusive range inclusive
- Do all of this while keeping the immutable nature of the code from the book
When trying to implement this, I discovered it was actually quite tricky. In particular, when using inheritance I ran into some obstacles:
- Unless we use the original range as a proxy, creating a new range based on the original is tricky. We basically need to clone, and that’s fraught in various ways.
MemberwiseClone
will work in many situations, but it’s inelegant – and we can’t keep the fields markedreadonly
and still modify the cloned copy. - Reversing a range using just the original type constraint of
T : IComparable<T>
is a bit of a pain. You need to keep remembering which way to compare things. This is a bit of an aside, but using anIComparer<T>
instead is a lot simpler – it’s really easy to build a new IComparer<T> which proxies to the original one and reverses the order of the parameters. - There’s no guarantee that just because the base class has no mutable data, the derived class will do likewise.
In addition, I realised I was using inheritance in a way that went against what I’d written near the end of the book: when using inheritance in a very limited way, consider using delegates instead. A Range<T>
only needs extra behaviour to be specified in terms of comparisons (IComparer<T>
) and how to take a step from one value to the next. The latter can easily be represented as a Func<T,T>
in .NET 3.5.
My new design has a single sealed, immutable class:
There are still a few ways in which this isn’t ideal:
- You can specify a null step function, in which case you can’t iterate over the range. I’d prefer the type to not implement
IEnumerable<T>
if it can’t do the job properly. - You have to specify a reverse step function if you want to iterate over the reverse of the range.
- There are a heck of a lot of constructor overloads.
Now, none of these are horrendous, and I think it’s a lot nicer than it was before. I’ve currently got an additional non-generic Range
class with a bunch of overloaded methods for creating ranges of various types. I can’t think of a decent name for these methods at the moment, so currently you’d write:
- Range.Of(1, 5) // 1, 2, 3, 4, 5
- Range.Of(1, 5).Exclusive() // 1, 2, 3, 4
- Range.Of(1, 5, 2) // 1, 3, 5
- Range.Of(DateTime.Today, DateTime.Now, TimeSpan.FromMinutes(1)) // Midnight, 1 minute past etc
I think it might be nicer to use extension methods for these, to allow:
- 1.To(5)
- 1.To(5).Exclusive()
- 1.To(5).Step(2)
- DateTime.Today.To(DateTime.Now).Step(TimeSpan.FromMinutes(1))
In order to do this nicely I may need to expose the comparer in Range<T>
as well, but I don’t think that’s really a problem. Thoughts on this are welcome.
Anyway, the broad point of this post (other than to hopefully relieve my own insomnia – and possibly yours too) is that immutability and inheritance don’t mix terribly nicely, especially when you want to effectively clone an instance and modify some aspects. That’s not terribly surprising, but it is interesting – and it fits in with the experience that inheritance doesn’t mix terribly nicely with equality comparisons, either.