Ranges in C#

I must admit that although I love the C# programming language dearly, I do sometimes find the thing lacking. And when that happens, I generally tend to either sulk or, better, go on crusades implementing things like monads just to have things my way. This article is yet another story about a feature I wanted, and this feature is called a range.

Note: there’s no source code accompanying this article. It’s all rather trivial, really.

Range? WTH are ranges?

Okay, let me briefly explain: say you’ve got a string and you want to remove its last character. What do you do? Well, the typical solution is to write

s.Substring(0, s.Length-1)

which is technically correct but feels like you’re hacking zombies with a scalpel (the opposite of ‘brain surgery with a chainsaw’). And in fact, certain languages such as Python (and Boo, by proxy) let you express this idea of slicing up arrays more succinctly. Specifically, you can use negative range values to measure distance from the end of the array. (Or from the end of one element past the array, as the case may be.)

Unfortunately, in C# we’re screwed by default, because there’s no way for us to replace the preceding expression with something nice like s[0:-1]. Or is there?

String Extensions

Let’s theorize for a little while. Say you’ve decided that it’s time to end the tyranny of Substring() with a simple extension method that somehow lets you enter all sorts of crazy values or even leave them blank

But this wouldn’t work because there already is a method called Substring() that takes two parameters (albeit differently named). So, what can we do? Well, we can give up and die, rename the method to something like SubString (may VB.NET users forgive us) or, better yet, attempt to encapsulate the whole concept of rangeness (or rangedness) in a separate structure.

Range Structure

publicstruct Range
{
publicint Start;
publicint End;
}

Great, right? We can now use this thing in our APIs, and the creepy extension method we wrote previously can now make more sense:

which I’m sure you agree is ugly and not worth the effort. We could narrow it down to a collection initialization Range{1,-2}, but this would require Range to implement IEnumerable which is unidiomatic and generally bad. There has to be another way.

Range Construction

And indeed there is a better way. Remember extension methods? Well, we can have them on any type including, you guessed it, an int. So, without further ado, I bring you the fluent range builder:

Trivial, right? You’ve now got a way of quickly defining a range with weird negative values (if you must) as follows:

string ss = s.Substring(1.to(-2));

Isn’t that better? And yes, I know it’s not perfect, but guess what, we live in an imperfect world.

Is that it?

The answer to this question is philosophical and depends on how much you actually want. I’ve shown a very simple example of using a range with the Substring extension, but generally, ranges are used to take slices out of arrays/matrices/vectors. For example, applying this concept to an enumeration, we could come up with something like:

The above takes a slice out of any IEnumerable (list, array, etc.) given the provided range. Obviously in a real scenario there’d be more rigorous checks on the range parameter, as well as obvious concerns related to materializing the whole collection. The above is for illustration purposes only.

It gets better, though. For example, what if that range parameter above was actually of type params Range[] instead? This would, as if by magic, allow you to take several slices out of one collection and concatenate them all together. Wouldn’t that be great?

But ranges are ultimately very fragile things, only really useful for denoting the start and end of something. What if you wanted, say, to assign the value of 0 to particular elements of an array? Would you create some Set() extension method? Perhaps, but there’s a more powerful alternative.

Views

A view is just a range plus the object it relates to. This difference is crucial because once you have both the range and the target object, you can do all sorts of naughty things like setting all the values in range to a particular value. Let’s flesh out this view thing. First of all, it’s pretty obvious what the class looks like:

Yep, I’m actually using two parameters in that indexer to represent the range more fluently. If you prefer to use x.to(y) notation, just create an overload. In fact, having one is a good idea for the general case. But wait, what about the setter? Well, on this particular occasion the only thing that will save you is a Set() method that would set the value of each element. Using the = operator doesn’t make much sense in this case.

Here are some other fun things you can do with views:

Implement set operations. Obviously these make sense only when the type of array referenced in the view is the same.

Implement IEnumerable. It kind of makes sense, that way you can iterate a view of an array.

Conclusion

In one of the Batman films, the Joker character says something along the lines of «see how much chaos you can do just with gasoline?». Well, this article is another illustration of how much you can do with extension methods – an unreasonably powerful feature that can let you spawn objets with magical powers.

Your mileage will, invariably, vary. But ranges can be useful, in ways that go beyond this article. Good luck and stay tuned for more unreasonable uses of extension methods!

Share

About the Author

I work primarily with the .NET technology stack, and specialize in accelerated code production via code generation (static or dynamic), aspect-oriented programming, MDA, domain-specific languages and anything else that gets products out the door faster. My languages of choice are C# and F#, though I'm open to suggestions.

I'm a Microsoft MVP (Visual C#) since 2009. I run a collective tech blog at DevTalk.net. I use my own editor called TypograFix to typeset articles and blog posts.

Like the article and want this implemented in your product? Got a project that can benefit from Microsoft.Net goodness? Then get in touch!

I think the end part is the dangerous one: is it including or excluding.
I.e. does end point to the last element or behind the last element (like in C++ iterators)?
I find the concept of end in ranges inferior compared to the count concept.
E.g. We have in C# Linq the method Enumerable.Range(begin, count). This is unambiguosly telling what the range is.
Cheers
Andi

Also, personally I don't like extension methods on string, int, double etc. because they crop up too often in Intellisense and you need to know the namespace to add the right using statement. Better IMHO is to have a static method on Range, maybe Range.Create(10,20).