In the following post I'm going to talk about two concerns I have with features planned for C# 8. Beforehand though, I just want to point out a couple of things:
C# is (and will continue to be) my favourite general-purpose language. In fact I wouldn't be writing this post if I didn't care about C#.
I'm not making these points because I think I definitely know better. They're just concerns that I haven't seen adequately addressed thus far. I actually semi-hope that someone will reply to this with reasons why these concerns are ill-founded.
There are also some really exciting features planned for C# 8 in my opinion, including non-nullable references, record types, async streams, improvements to pattern matching, and more; none of which I'm going to be talking about below.
Also for previous subscribers, I'm sorry for the huge hiatus in blogging. I've been busy with work and all my spare time has been going in to patching
my game. That's finally done however so hopefully normal service will be resumed. :)
Anyway, let's crack on...
C# Proposal Champion: "slicing" / Range
Further discussion
The proposal is essentially adding two new operators to C#:
public static void Main() {
var intArray = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var arraySubrange = intArray[3..7]; // Array subrange is now a 'view' on to intArray that includes the values 3, 4, 5, 6
var lastElement = intArray[^1]; // lastElement is 9
}
View Code Fullscreen • "Slicing and Hat Operator Proposed Syntax"
The range operator (
3..7) selects a subrange of the indexed collection. At the moment the consensus is that the returned object would be some kind of struct that implements
IEnumerable<T>; and that it would be simply a 'view' on to the source collection, and
not invoke a copy operation of the targeted elements.
The hat operator (
^1) indexes the target collection from the
end of the sequence, rather than the start.
Firstly, I think the overall proposal is generally a good thing. I'm not convinced it's something people are crying out for; although if they decide to allow non-constants in the range expression (e.g.
var arraySubrange = intArray[startIndex..endIndex]) I will certainly re-evaluate that opinion.
Edit: Jared Par confirmed on Reddit that non-constants will be permitted in range expressions. Personally I think that makes this feature actually very strong and will allow certain algorithms to be written with much greater terseness.
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].
I think the main justification for this is to intuitively support the following scenarios:
const int ArraySize = 10;
public static void Main() {
var intArray = new[ArraySize] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var arraySubrange = intArray[0..ArraySize];
arraySubrange = intArray[0..^0]; // Same result
}
View Code Fullscreen • "Potential IOOBException"
With
exclusive indexing, the code above works very nicely; whereas with
inclusive indexing you need a -1 on the
ArraySize:
const int ArraySize = 10;
public static void Main() {
var intArray = new[ArraySize] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var arraySubrange = intArray[0..ArraySize-1];
arraySubrange = intArray[0..^0]; // Same result
}
View Code Fullscreen • "No IOOBException with inclusive indexing"
However, I personally think that's a pattern that many veteran programmers are familiar with. Yes, it's less terse than its alternative, but I think there is
already a collective understanding amongst coders that collections end at count-1.
Furthermore, the
exclusive default currently being proposed makes other algorithms less intuitive. Take this example of a function that determines whether a string is a palindrome (taken from
Joe4evr):
// With inclusive indexing
public static bool IsPalindrome(string input) {
for (int i = 0; i < input.Length; i++) {
if (input[i] != input[^i])
return false;
}
return true;
}
// With exclusive indexing
public static bool IsPalindrome(string input) {
for (int i = 0; i < input.Length; i++) {
if (input[i] != input[^(i + 1)])
return false;
}
return true;
}
View Code Fullscreen • "Hat operator palindrome func"
For me, this example perfectly highlights the underlying issue:
Programmers are used to collections being 0-based, and violating that assumption is counter-intuitive, even if we're talking about a new operator.
In other words, just because we're indexing from the back doesn't mean we should go against that habitual behaviour that sequences start at 0.
Another example: If I reverse a sequence (e.g. with
var reversed = intArray.Reverse().ToArray()) I can then get the last element from the original array with
reversed[0]. But if I access the elements from the back with the hat-operator, I have to use
intArray[^1].
Discussion is actually still ongoing about this on github, but I got the chance to ask Mads Torgersen (the C# Language Project Manager) about this via the recent .NET Conf live stream; and
his reply indicated that
intArray[^0] is currently planned to point past the end of the array (i.e.
intArray[^0] will always throw an IndexOutOfBoundsException).
One justification he gave is that this means the range expression
intArray[0..^0] will intuitively return the entire array. But my contention is that this is a kind of circular logic; if the hat and range operator were both
inclusive, you'd still get the same nice result. The problem would only occur if the hat-operator was inclusive while the range operator remained exclusive.
One side-note I'd like to add is that I don't see this as a particularly cut-and-dry issue. I've tried to present the case both for and against these operators being inclusive, and I'm very open to the idea that someone might show me a convincing reason I'm wrong. So please, do form your own opinions. I'm also not particularly comfortable being in the realm of 'disagreeing with Mads Torgersen' about something. Oh well, if you don't stick your neck out sometimes you'll never see over the fence... :)
There are also good arguments against my concerns. See for example
this Reddit comment by /u/TheBuzzSaw:
"It's pretty important that the range be inclusive-begin exclusive-end. So, an empty range is 0..0. [...] Having an exclusive-end makes many many operations really simple. When processing fixed width chunks, you simply add the chunk size: i..i+n. Having an inclusive-end would result in i..i+n-1."
C# Proposal: Default Interface Methods
C#: A Tour of Default Interface Methods
No prizes for guessing this one was gonna come up. To be honest, I think most community-invested C# programmers are well aware of this proposal and already have strong opinions on it. If you don't know what it is, simply follow the links above.
Firstly let me start by saying I'm actually generally on-board with the idea. This
should be a way of getting
traits in to C#, which is something that many, many other modern languages already have:
And lots more
Some people have argued that it changes the meaning of what an interface is, and/or that we already have abstract classes for this purpose, and/or that it's a form of multiple inheritance...
The 'meaning' of an interface does not change. It is-and-remains a construct that forward-declares a set of methods any class must implement for its instances to be usable as the thing the interface represents. Just because a certain method on an interface has a default implementation shouldn't mean a thing to outside consumers of that interface. Only the implementer of the interface need worry.
Abstract classes can contain state, and classes will remain only-singularly-inheritable. If they did not, other issues would arise (such as the infamous
diamond of death). Interfaces contain
no state (properties are not state, they are
methods that look like state) and therefore retain a completely distinct and useful value in C#.
Multiple Inheritance is not in itself always a bad thing, and received a bad name in the wild-west days of C++; but traits are a form of MI and are used daily to great success.
There are also
other issues some people have which I can't claim to have an opinion on; I suggest reading through some of the comments on
the tour post.
It's really only one concern, but it's a big one for me: This proposal
is being touted as an implementation of traits for C#, but in my opinion it's a lousy one. Why? Because
default behaviour will not be implicitly imported in to classes. Here's an example:
interface IEntity { // interface representing an entity in a game world
Vector3 Position { get; }
}
interface ISoundEmittingEntity : IEntity { // represents any entity that emits sound
float SoundRadius { get; }
// method with a default (sensible) implementation; can be overridden for more complex cases
bool IsAudibleToPlayer(Player player) => Vector3.Distance(player.Position, Position) <= SoundRadius;
}
class Rocket : ISoundEmittingEntity {
public Vector3 Position { get; private set; }
public float SoundRadius { get; } = Units.InMetres(4f);
}
// ----
void SomeMethod(Rocket rocket) {
if (rocket.IsAudibleToPlayer()) PlaySound(Sounds.RocketSound);
}
View Code Fullscreen • "Traits example"
The problem with the code above is that it won't compile.
IsAudibleToPlayer has to be explicitly imported in to
Rocket, even if it's just the default implementation.
Currently, the proposed syntax for this would be:
class Rocket : ISoundEmittingEntity {
public Vector3 Position { get; private set; }
public float SoundRadius { get; } = Units.InMetres(4f);
public bool IsAudibleToPlayer(Player player) => ISoundEmittingEntity.base.IsAudibleToPlayer(player);
}
View Code Fullscreen • "Current traits proposal"
Frankly I don't find this particularly useful, and it doesn't really resemble any traits implementation I'm familiar with. We can already implement an almost identical pattern with what's available today:
interface IEntity {
Vector3 Position { get; }
}
interface ISoundEmittingEntity : IEntity {
float SoundRadius { get; }
bool IsAudibleToPlayer(Player player);
}
public static class SoundEmittingEntityDefaultImplProvider {
public static bool DefaultIsAudibleToPlayer(ISoundEmittingEntity @this, Player player) {
return Vector3.Distance(player.Position, @this.Position) <= @this.SoundRadius;
}
}
class Rocket : ISoundEmittingEntity {
public Vector3 Position { get; private set; }
public float SoundRadius { get; } = Units.InMetres(4f);
public bool IsAudibleToPlayer(Player player) => this.DefaultIsAudibleToPlayer(player);
}
View Code Fullscreen • "Traits example"
In fact, with extension methods we can actually get even closer to real traits,
as I demonstrated 2 years ago.
The current response from Microsoft is that
we should use interfaces as our 'leaves' (i.e. using
IRocket everywhere instead of
Rocket; and then only using the actual
Rocket class to instantiate).
But that doesn't make much sense to me. I don't see why it's any better: We still have to awkwardly re-import default behaviour somewhere, and yet the leaf interfaces still
look as though it's done automatically. Why?
Furthermore, if you envision an order of magnitude more leaves than trait types that's probably way more trouble than it's worth.
And finally, as a way of reducing boilerplate/code duplication, existing patterns like the ones I enumerated above already exist and are already more terse, imo.
Personally, it feels to me like the default interface methods proposal is
actually primarily there to support an entirely different use-case: Allowing interfaces to be modified after going public, and compatibility with languages like Java (which as far as I understand, would make Xamarin users' lives a lot easier). And what I wish Microsoft would do is either:
Drop the whole claim of support for traits entirely from this proposal, and embrace fully the fact that this isn't the primary objective, or...
Revisit its applicability as a traits implementation and at least allow us some kind of syntax to auto-import default members (e.g. something like [ImportDefaults(typeof(ISoundEmittingEntity))] class Rocket : ISoundEmittingEntity), if not hopefully making it the standard behaviour.
Thanks for reading!
Edit 15th Sep '18: Jared Par confirmed on Reddit that non-constants will be permitted in range expressions
Edit 15th Sep '18: Fixed an error in the traits code (replaced IsAudibleToPlayer with DefaultIsAudibleToPlayer)
Edit 15th Sep '18: Fixed an error in showing inclusive vs exclusive differences for range selection
Edit 16th Sep '18: Added a link to /u/TheBuzzSaw's reddit comment in the side-note subheading of the range operator section