Just to be clear, despite the title of this post, I love C# and will continue using it for as long as I can. In my opinion, it is the best designed general-purpose programming language available today. No, it's not perfect (as noted by the existence of this post), but I think C# does the least amount wrong, and it is a joy to work in.
The purpose of this post is simply to explore what kind of breaking changes would be worth making to the language if the opportunity were to present itself. I am a fan of programming language design. To me, this is just an exercise in design and evaluating the consequences of such design decisions. Not everything listed below is a breaking change; the assumption is that the change would deprecate a bunch of other APIs/behavior, which would be removed.
UTF-8 Everywhere
Historically, the choice to use UTF-16 for strings in Java and C# makes sense. I'm not mad about it, but today, the best choice is definitely UTF-8. It often has higher byte-density than UTF-16 (especially considering the programming language itself is English, as are popular protocols like HTTP). The new System.Text.Json
APIs specifically provide ways to bypass conversion to and from string
because the world outside of C# (mainly the web) has largely settled on UTF-8. The continued use of UTF-16 is a performance liability at this point.
Methods Outside Classes
Not much to say here. I want to be able to make free-floating methods. Stop insisting everything I do be inside a class
or struct
. OOP is great, but this requirement was an overreach. While we're at it, can I avoid indenting my entire file inside a namespace? Just let me do this up top:
namespace Acme;
Reader + Writer Paradigm
I really like the trend in recent .NET APIs to separate data types into explicit reader and writer types. Take a look at Channel<T>
. The class is nothing more than a tuple of ChannelReader<T>
and ChannelWriter<T>
. This allows me to pass just the reader or just the writer into a method. It is much clearer what can and cannot happen. I want to take this concept and spread it just about every other corner of the language. I would love to have an ArrayReader<T>
or a DictionaryWriter<TKey, TValue>
. There were attempts at this in the early days with things like IReadOnlyCollection<T>
, but they have two problems. First, the names are dishonest. An IReadOnlyDictionary<TKey, TValue>
is not a read-only dictionary; it is simply read-only in a small context, but code elsewhere may very well be writing to it (possibly even concurrently). Second, it's often not that hard to simply cast to a read-write type. Obviously, that'd be a conventional no-no, but the point is that having a clean separation between Reader<T>
and Writer<T>
puts distance between the two modes.
With this design in place, APIs could be much clearer about their intent. There are many APIs, for example, that accept IDictionary<TKey, TValue>
. Will it write to that dictionary? Probably not, but who can say for sure? I'd feel a lot better if I could send around IDictionaryReader<TKey, TValue>
. In cases like ImmutableDictionary<TKey, TValue>
, there simply wouldn't even be an IDictionaryWriter<TKey, Value>
anywhere! You'd call transformation methods that return a new IDictionaryReader<TKey, TValue>
.
The longer I code in C#, the more I wish arrays were immutable. I'm not fully sold on F# or functional programming in general, but if some kind of ArrayReader<T>
were available (again, such that it couldn't just be cast to the writer version), I'd like that. So many legacy APIs (and even new ones) accept T[]
, which means I am at the API's mercy as to whether things inside will change. It feels like a subtle inconsistency that strings are immutable while arrays are not, but I get why it played out that way.
Note that I am aware of ReadOnlySpan<T>
and ReadOnlyMemory<T>
. They are great but do not fully accomplish what I described above.
Strict Nullability Rules
I love that C# 8 is tackling the problem of null references. I am adopting it into my current projects with great success, but this really should have been there from the beginning. The issue with C# 8 is that it is an opt-in mechanic that must remain compatible with code that did not opt-in. Again, I'm not criticizing the .NET team: adding this kind of feature this late into the ecosystem is a huge undertaking, but this post is about my dream version of C#. :)
I know people are a fan of things like Maybe<T>
or Optional<T>
. I've just always been a fan of leveraging null
for that purpose. How great would it be if every single API used ?
to denote nullability? Let's get rid of all that code that checks for null
(unless, of course, you're working with the T?
variant). I think I would even do things a little differently from C# 8. If I have a string? data
variable, rather than trying to make the compiler super duper smart about if or when the variable is null
, I think nullable types should have a one-way conditional cast to a non-nullable type. I'm not sure what the code would look like.
// Maybe something like this...?
void DoTheThing(string? data)
{
if (data.TryGetValue(out string nonNull))
{
// nonNull would only be visible inside here.
}
// nonNull could not be used here.
}
I just think it would be simpler both for humans and compilers to understand where the transition happens and what contexts the non-null value exists in. The variable data
would be more of a wrapper than a value (much like how they are for Nullable<T>
) with fewer implicit casts to the non-null variant.
defer
keyword
There is actually an open issue for this feature right now. I'm excited, provided it is done right. This keyword would effectively become a fancy finally
block to help deal with objects that do not follow the IDisposable
flow. So, today, you write code like this:
semaphore.Wait();
try
{
// Do all the things.
}
finally
{
semaphore.Release();
}
I don't like that the entire body of work is contained in that try block, which also results in another level of indentation. I would much rather write code like this.
semaphore.Wait();
defer semaphore.Release();
// Do all the things.
There are workarounds today, but I do not like that they largely entail extra objects and/or delegates.
semaphore.Wait();
using var sr = new SemaphoreReleaser(semaphore); // Custom type.
// Do all the things.
At least, in C# 8, it still avoids the new block/indentation thanks to the freestanding using
.
To Be Continued
I'm sure I'll think of more ideas to share in the future. I can turn this into a series if there's interest. Feedback welcome! Have a great day!
Top comments (4)
Nice post. 👍
Especially the nullable ref-types feel very tacked on. If they had been there from the start the differences between those and Nullable would probably have been less jarring.
Yup! To be fair, they don't just "feel" tacked on; they were actually tacked on in the most recent version of C#. That's hard to reconcile against the years and years of existing C# code. At least they're trying!
Reduce use of exceptions in general. Some functions return an error value, others throw. Very annoying.
I agree and disagree. I've come to appreciate the methods that throw. At minimum, each method that can fail should have both a throwing and non-throwing variant (
Parse
vsTryParse
). .NET has been improving in this area (by way of things likeHttpClient
returning a status code and letting you choose whether to throw via helper method).