Why doesn't the new hat-operator index from the C# 8 array-slicing feature start at 0?

C# 8.0 introduces a convenient way to slice arrays - see official C# 8.0 blogpost.

The syntax to access the last element of an array is

var value = new[] { 10, 11, 12, 13 };

int a = value[^1]; // 13
int b = value[^2]; // 12

I'm wondering why the indexing for accessing the elements backwards starts at 1 instead of 0? Is there a technical reason for this?


Solution 1:

Official answer

For better visibility, here is a comment from Mads Torgersen explaining this design decision from the C# 8 blog post:

We decided to follow Python when it comes to the from-beginning and from-end arithmetic. 0 designates the first element (as always), and ^0 the “length’th” element, i.e. the one right off the end. That way you get a simple relationship, where an element's position from beginning plus its position from end equals the length. the x in ^x is what you would have subtracted from the length if you’d done the math yourself.

Why not use the minus (-) instead of the new hat (^) operator? This primarily has to do with ranges. Again in keeping with Python and most of the industry, we want our ranges to be inclusive at the beginning, exclusive at the end. What is the index you pass to say that a range should go all the way to the end? In C# the answer is simple: x..^0 goes from x to the end. In Python, there is no explicit index you can give: -0 doesn’t work, because it is equal to 0, the first element! So in Python, you have to leave the end index off completely to express a range that goes to the end: x... If the end of the range is computed, then you need to remember to have special logic in case it comes out to 0. As in x..-y, where y was computed and came out to 0. This is a common nuisance and source of bugs.

Finally, note that indices and ranges are first class types in .NET/C#. Their behavior is not tied to what they are applied to, or even to be used in an indexer. You can totally define your own indexer that takes Index and another one that takes Range – and we’re going to add such indexers to e.g. Span. But you can also have methods that take ranges, for instance.

My answer

I think this is to match the classic syntax we are used to:

value[^1] == value[value.Length - 1]

If it used 0, it would be confusing when the two syntaxes were used side-by-side. This way it has lower cognitive load.

Other languages like Python also use the same convention.