Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Ranges in C#

0.00/5 (No votes)
1 Feb 2013 1  
Introducing a well-known concept of ranges implemented 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

public static string Substring(this string obj, int start = 0, int end = -1)
{
  if (end < 0) end = obj.Length + end;
  return obj.Substring(start, end-start);
}

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

public struct Range
{
  public int Start;
  public int 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:

public static string Substring(this string obj, Range range)
{
  if (range.End < 0) range.End = obj.Length + range.End;
  return obj.Substring(range.Start, range.End - range.Start);
}

However, the invocation is still creepy because we need to write something like

string s = "abcde";
string ss = s.Substring(new Range{Start=1,End=-2});

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:

public static Range to(this int start, int end)
{
  return new Range {Start = start, End = end};
}

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:

public static IEnumerable<T> At<T>(this IEnumerable<T> obj, Range range)
{
  var list = obj.ToList();
  if (range.Start < 0) range.Start += list.Count;
  if (range.End < 0) range.End += list.Count;
  return list.Skip(range.Start).Take(range.End - range.Start);
}

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:

public class ArrayView<T>
{
  public Range Range;
  public T[] Array;
  public ArrayView(T[] array, Range range)
  {
    Array = array;
    if (range < 0) // you know the drill
    Range = range;
  }
}

I’m using an array for illustration purposes, something else could be there! The point is that now instead of an At method we can have a View method:

public static ArrayView<T> View<T>(this T[] array, Range range)
{
  return new ArrayView<T>(array, range);
}

The view acts on a pretransformed range (negative values, remember?), but what does it let us do? Well, for starters we can implement a basic indexer:

public T this[int i]
{
  get {
    // don't forget to pre-transform, then
    return Array[Range.Start + i];
  }
  set {
    // same here, then
    Array[Range.Start + i] = value;
  }
}

And here’s how you would use it:

var a = new[] {1, 2, 3, 4};
var b = a.View(2.to(3));
b[0] = 42;
Console.WriteLine(a[2]);

And yes, a[2] really does equal 42, as you would expect. 

Fun with views

So what can you actually do with views? Well, how about views-of-views? After all, there’s nothing stopping us from having yet another indexer that takes another Range, so…

public ArrayView<T> this[int start, int end]
{
  get
  {
    // pretransform, as always
    return new ArrayView<T>(Array,
      new Range
      {
        Start = start + Range.Start,
        End = end + Range.End
      });
  }
}

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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here