A feature wishlist for C#

symbiogenesis

Edward Miller

Posted on July 27, 2024

A feature wishlist for C#

C# is very near to a perfect programming language, by my standards. But there are still some rough edges.

There has been much discussion about proposals such as extension types or the field keyword inside getters and setters, which are mostly about convenience.

And there's been a lot of work on source generators and AOT, which is mainly about performance.

But I think we need a focus on correctness. Here are some examples:

Real Nullable Types

The null reference exception is a legacy of the "billion-dollar mistake" that Tony Hoare said he made in the design of the ALGOL W language in 1965. We have been dealing with it ever since.

Nullable types ought to be an authentic part of the type system in C#. They currently appear to be like a thin shim of syntactic sugar that are little more than Roslyn analyzers.

If you make a public function that requires a non-nullable parameter that is a reference type, and call it from another project that doesn't enable nullable types... there is actually nothing stopping the parameter from being filled with null.

No error would be thrown at compile-time or runtime when trying to fill that parameter with a null.

So, despite that you have already clearly specified your intentions, you have to guard your public parameters against null references.

And if you try to detect whether a type is nullable or not, using reflection, then a reference type is going to always be seen as nullable. As far as I know, there is not even a way to write a generic function that detects whether you defined a reference type as non-nullable. Because no matter what you actually do it really is nullable.

All of that works fine for value types, but that still leaves way too much room for covert null reference exceptions.

Better type inference for generics with constraints

We should be able to do complex constraints where one generic type is a collection of another generic type. This helps broaden the expressivity of the type specifications, and thereby more precisely define what it is the program ought to be doing at compile-time.

We should want this for all the same reasons we like strong types in general.

see also: https://github.com/dotnet/roslyn/pull/7850

Exception Type Matching

All possible exception types that can be thrown by a particular function call should be known at compile-time, much like Rust's concept of the Option type.

Analyzer CA1031 exists specifically to inform the user that they ought not be catching general exceptions.

A better language would be able to show you precisely in the IntelliSense popup all possible exception types that could be thrown, even without the library authors documenting the exceptions with XML comments.

If this were the case this would help the specific kinds of exceptions to be handled in the specific ways that they ought to be, without any fear of unknowns.

In Rust, you can do a switch statement upon all the possible exceptions, which makes it really clear when one of the possible exceptions wasn't considered.

In C#, baking this into the normal try-catch syntax would be fine, and then turning on CA1031 by default would be fine, as long as IntelliSense would always and reliably tell you exactly what exception types are missing. As it does with a switch statement on an enum.


And, aside from correctness-oriented improvements, there are some oversights in existing features need to be addressed:

Kotlin-style init blocks

If you want to add some imperative logic during initialization, you cannot currently use a primary constructor. Kotlin solved this with the init block. C# should copy that.

see also: https://github.com/dotnet/csharplang/discussions/4025


And some general .NET Framework enhancements are needed in the standard libraries:

Range support in ICollection

This one is super obvious. ObservableCollection is a great example of where we need it. There have been countless custom implementations of something like ObservableRangeCollection. Having all of the changes show up in a single CollectionChanged event makes vastly more sense than users falling back to using loops of Add() calls because the framework has this deficiency.

see also: https://github.com/dotnet/runtime/issues/18087

Deserializing interfaces with System.Text.Json

Newtonsoft.Json supports this by default without much effort on the part of the developer. System.Text.Json now has some support for "polymorphic deserialization," but it involves a lot of manual intervention on the part of the programmer.

Apparently, there are security concerns around this, and the current approach certainly allows for improved performance. But for quick and dirty stuff, it would be nice to have a simple default that just works for most cases. While still giving some safety valves against attack vectors, like attempting to (de)serialize Exception types.

ConfigureAwait() improvements

Library authors generally want to use ConfigureAwait(false) everywhere, but there is no easy way to define this globally. Having the codebase littered with ConfigureAwait(false) everywhere is ugly, and some async function calls are likely to be overlooked and thus defeat the entire point.

Turning on analyzer CA2007 can help, but you would need to intentionally do so, and know to do so. And it is still annoying.

There must be a better way.

💖 💪 🙅 🚩
symbiogenesis
Edward Miller

Posted on July 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related