Ben Bowen's Blog • Home / Blog • • About • • Subscribe •

C# 8 Concerns - A Followup 

About two months ago I made a post explaining some concerns I had with two new features in C# 8 (the range/hat operator and default interface methods). If you haven't read that post, I recommend reading it first before coming back here: C# 8 Concerns.

The post generated a lot of discussion/feedback. I've wanted for a while to write a quick followup post addressing and acknowledging some of that, and now that Mads Torgersen just published a blog post detailing the upcoming C# 8 features it feels like the right time.

Range Operator and 'Hat' Operator 

My concern was with the exclusive nature of the range-end/hat operator:

"My issue is that both the hat operator and the end-element of the range operator are currently 'exclusive'. I.e. intArray[3...7] returns a view onto the elements 3, 4, 5, 6 but not 7; and to get the last element of an array the correct syntax is intArray[^1], not intArray[^0]."

Currently the feature is still planned to use exclusive-indexing for both the end-range operator and the hat operator:

Index i1 = 3;  // number 3 from beginning
Index i2 = ^4; // number 4 from end

int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Console.WriteLine($"{a[i1]}, {a[i2]}"); // "3, 6"

var slice = a[i1..i2]; // { 3, 4, 5 }
View Code Fullscreen • "Latest range/hat operators example taken from "Building C# 8.0""
Of the two concerns raised in the original post, this one seemed to generate the most discussion. Although a lot of people agreed with what I wrote, roughly as many disagreed; and there were some good points raised in defense of the operators being 'exclusive'.

Upon reflection, I have to admit I think I may have been wrong about this; and that maybe these operators should be exclusive. Here are some of the reasons why:

Writing code that splits a collection in to N-sized chunks is easy: a[i*ChunkSize..(i+1)*ChunkSize].
Specifying a range that creates a view onto an entire collection is as simple as a[0..a.Length] (compared to a[0..a.Length-1]). In itself a pointless thing to do, but this fact could make it easier to deal with bounds defined at run-time: No need to deal with tricky -1s everywhere.
Exclusive bounding makes it trivial to specify an empty range: a[0..0]. With inclusive we'd have to write a[0..-1] which doesn't feel natural at all (credit /u/TheBuzzSaw).

A lot of other operations we're used to using in .NET and programming in general are exclusively-upper-bounded. For example, any operation that takes a length/count of elements is using that as an implicit exclusive-upper-bound (e.g. "1234".Substring(0, 3) is "123").

Even the way we write for-loops are exclusively-upper-bounded (e.g. in the loop for (var i = 0; i < UpperBound; ++i), i will never reach UpperBound).

In fact, it's probably something fundamental to computer science.

...On the other hand, there are still some caveats that I want to at least mention:

When creating a range of values rather than a range of indices, exclusive upper bounds are usually not what we want. For example, a[0..9] will be 0 1 2 3 4 5 6 7 8 but not include the 9.
This seems like a non-issue for something as trivial as a[0..9], but as a further example consider accessing a list of enum values. Assuming behind-the-scenes they're defined as consecutive values, it's inelegant to create the range such that it encapsulates every value: (int)DayOfWeek.Monday..(int)DayOfWeek.Sunday+1. This is necessary because DayOfWeek.Sunday would otherwise be an exclusive upper-bound, and be left out of the range.
Exclusive bounding is less elegant for algorithms that work from either 'end' of a collection (such as the palindrome example in the original post) or any function that could optionally iterate over a collection in reverse. This is because a[m] is not the same as aReversed[^m]. Instead, a[m] is equal to aReversed[^(m + 1)].

Still, I don't think these are really deal breakers. If we take the ^ token to be read as "array.Length - " (i.e. '^7' becomes "array.Length - 7"), I think it can work okay. I have to admit, I have written more than my fair share of array.Length - 1s in the past, so replacing that with ^1 will be a nice time saver either way.

It does still feel like the hat operator would be nicer if it could start at ^0 in some cases, but by necessity that would require changing the range operator to be inclusively-bounded on both sides to make a lot of other stuff work. And honestly, I think that's a much worse compromise.

So in conclusion, exclusive-range-upper-bounds and exclusive-hat-operator-indexing are probably the right approach.

Default Interface Methods 

My concern was simply that default behaviour from interfaces will not be implicitly imported in to classes that implement those interfaces. The only way to access the default behaviour will be to use the object specifically as the interface type. In other words, myClass.SomeDefaultMethod() won't compile by default; we have to use ((IMyInterface) myClass).SomeDefaultMethod(). That is unless MyClass actually provides an implementation for SomeDefaultMethod.

From what I can tell, this is still the planned behaviour. Another way to look at this is to imagine that default-methods are always explicitly implemented on implementing classes. Only if the class redeclares the method publicly will it be visible on instances of that class.

The thing is, I'm actually perfectly happy with this behaviour when viewing the feature only as a way of allowing interfaces to change over time without breaking backwards compatibility. It's just that, at the time, this feature was also being touted as an implementation of traits for C#. And that is something I'm still certain about: This would be a lousy traits implementation.

Ultimately, I stand by what I said in the original post: The best thing to do here would be to drop any mention of traits from this feature and make it clear that this is intended only to support interface flexibility for published APIs.

It remains to be seen if this is what will happen, but there was no mention of 'traits' or any kind of boon to polymorphism in the "Building C# 8.0" section about this feature, so it's possible. There is still a lot of discussion on the proposal champion page regarding its applicability as an MI/traits feature however. And it seems some users are basically asking for the same thing I am.

Summary 

In summary, the hat/range operators are probably good-as-planned; and the default-interface-methods proposal is fine (to me at least) so long as we don't start calling it an implementation of traits for C#.

As always, thanks for reading. And like last time, I'd like to finish by saying I'm still very happy with the overall featureset planned for the future. The C# design team do a great job and I'm still looking forward with positivity to C# 8.