﻿# Brave new C\#

Next month, another version of our beloved programming language is set to arrive\. Nice reason to grab a cup of coffee and dwell on what is wrong with modern versions of C\#, isn't it? 

![1348_brave_new_csharp/image1.png](https://import.viva64.com/docx/blog/1348_brave_new_csharp/image1.png)


> We published and translated this article with the copyright holder's permission\\\. The author is Maxim Dobroselsky \\\(\[melanchall\]\(https://github\.com/melanchall\)\\\)\\\.




> \*\*Translator's note:\*\* The original article was written before the release of \\\.NET 10\\\.



## Introduction

Back in my student years, I had a book by a rather famous Dane about a rather famous programming language\. And although I harbored genuine warm feelings for C\+\+, I couldn't get through even half of that book—the reading was that dreary\. Well, it didn't stop me from continuing to believe in the destiny fate had in store for me and C\+\+\. So, in the summer of 2011, I went to interview for a C\+\+ developer\.

This is what I got from the interview: I didn't know C\+\+\. But I was offered a chance to try my hand at C\#\. My pride was wounded, but I agreed: I was eager to start working\.

The test task was to write a "Snake" game\. To make it more complex, the field was a map of Russia, movement was restricted to its borders, and the snake could only grow by eating major cities\. The game was supposed to run in a browser using the now\-departed [Silverlight](https://en.wikipedia.org/wiki/Microsoft_Silverlight)\.

I wrote the application, it was approved, and I was hired for my first job, while C\+\+ remained in the past\. This is how, by the will of fate, I became a C\# developer, grew to love this language, and remain inseparable from it to this day\. Unfortunately, I sometimes get the feeling that not everything is quite right with it\.

## Is there a problem?

Sooner or later, any programmer fascinated by C\+\+ concludes: _"No one knows this language completely\."_ Even though my path with C\+\+ diverged many years ago, this thought has started resurfacing in recent years\. Though in a slightly changed form: instead of \+\+, I now see \#\. Of course, C\# is still very far from the complexity of Bjarne Stroustrup's creation\. However, many innovations are beginning to raise concerns about the C\# former elegance\.

But before we start carefully dissecting syntactic constructs on the laboratory table, let me address a perfectly fair remark: _"No one is forcing you to use new features\."_ That is, there is no actual problem, so why start this whole discussion? The interest here is rather personal\.

As a developer of the [\.NET library](https://github.com/melanchall/drywetmidi), I sometimes face user requests for the API extension\. Sometimes I see potential in someone else's code for new functions or reworking old ones\. But are these changes justified? How much will the API complexity increase? Is solving certain tasks with the library's current design really that inconvenient?

Balancing between concise contracts and solving real user problems with simplicity is a constant tightrope walk\. And I'm very curious how the team at Microsoft handles this balancing act while developing a tool I use every day\.

Of course, any programming language must evolve\. And let's be honest, I myself use many of new features\. But was it really that difficult and inconvenient without them? Even though my criticisms may sound ultra\-conservative, like _"Back in my day, bytes were more eight\-bit,"_ I'd like to share my thoughts and discuss them with you\.

## The starting point

According to the [official chronicle](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history), C\# has had 17 revisions up to the time of writing this article\. It's interesting to note that the first seven versions \(up to and including the sixth\) span a period of 13 years, while the remaining ten were released in just 8 years:

![1348_brave_new_csharp/image2.png](https://import.viva64.com/docx/blog/1348_brave_new_csharp/image2.png)

It's worth noting that, unlike the seemingly chaotic early release schedule, nowadays we can expect new language versions every November, as they are tied to [\.NET releases](https://dotnet.microsoft.com/en-us/platform/support/policy):

![1348_brave_new_csharp/image3.png](https://import.viva64.com/docx/blog/1348_brave_new_csharp/image3.png)

The explosive growth of features in C\# began roughly between the seventh and eighth versions\. Here's a graph showing the increase in the number of keywords:

<details>
   <summary>How I collected data</summary>

1. The source code for the [C\# Keywords](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords) page from the official documentation is in the file [docs/docs/csharp/language\-reference/keywords/index\.md](https://github.com/dotnet/docs/blob/main/docs/csharp/language-reference/keywords/index.md)\. So, I used commits to this file and compiled lists of keywords\.
1. But commits only go back to November 2016\. Data on the list of keywords before this date was collected manually from MSDN archives\.
1. At certain points in time, the same keywords were counted differently depending on context—for example, `partial` when declaring classes and methods\. I didn't make this distinction during data collection\.


</details>
![1348_brave_new_csharp/image4.png](https://import.viva64.com/docx/blog/1348_brave_new_csharp/image4.png)

We can also look at the contents of the [Syntax\.xml](https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml) file in the [dotnet/roslyn](https://github.com/dotnet/roslyn) repository\. It's used to generate the Roslyn compiler and contains all syntax rules for C\#\. Let's start counting from the Visual Studio 2015 RTM release, July 2015, when Roslyn [became the default compiler](https://learn.microsoft.com/en-us/visualstudio/extensibility/roslyn-version-support) in VS—it started supporting all language features\. This is how the number of nodes in that file has changed over the years:

<details>
   <summary>How I collected data</summary>

1. I used commits of the [Syntax\.xml](https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Syntax/Syntax.xml) file\. The file was moved/renamed twice in its history; changes were tracked back to the first version of the source document\.
1. For each commit, the number of child nodes of the `Tree` element was collected\.


</details>
![1348_brave_new_csharp/image5.png](https://import.viva64.com/docx/blog/1348_brave_new_csharp/image5.png)

In addition to Microsoft actively developing the language itself, the company also made it possible to propose features and vote for them in the corresponding GitHub [repository](https://github.com/dotnet/csharplang)\. So many rushed to create requests describing their professional pain and ways to solve it through new language constructs\. I confess, I also have such proposals to my name \([first](https://github.com/dotnet/csharplang/issues/742) and [second](https://github.com/dotnet/csharplang/issues/842)\)\. And I'm very glad they were rejected\.

If you go to the [proposals](https://github.com/dotnet/csharplang/tree/main/proposals) directory, the subdirectories start from the sixth version\. But that [directory](https://github.com/dotnet/csharplang/tree/main/proposals/csharp-6.0) only contains three small files\. Probably, these were the internal wishes of the language development team\. However, in the [directory](https://github.com/dotnet/csharplang/tree/main/proposals/csharp-7.0) for the seventh version, we already find documents with links to discussions\. Here we arrive at the starting point when debatable new C\# features began to appear\. Well, it's just my completely subjective and non\-authoritative point of view\.

Another curious point worth noting is an [ECMA\-334](https://ecma-international.org/publications-and-standards/standards/ecma-334) standard, describing full specification of C\#\. At the time of writing, the last published version is the seventh\. And it doesn't contain, for example, the keyword [`and`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#logical-patterns)\. This keyword was introduced back in C\# 9 four versions ago\. Moreover, in the official [repository](https://github.com/dotnet/csharpstandard) where the standard is developed, there are two branches: [draft\-v8](https://github.com/dotnet/csharpstandard/tree/draft-v8) and [draft\-v9](https://github.com/dotnet/csharpstandard/tree/draft-v9)\. Even in the draft of the ninth version, the language grammar description [file](https://github.com/dotnet/csharpstandard/blob/draft-v9/standard/grammar.md) lacks `and`\. So, the official specification and even its new edition drafts [can't keep up](https://github.com/dotnet/csharpstandard/issues/1390) with the actual development of C\#\.

## When you just can't stop

Let's look at [local functions](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/local-functions), added in [C\# 7\.0](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-70)\. They look like ordinary methods, just without access modifiers\. So, perhaps we can make them static? Starting from version [eight](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-80), we can, says Microsoft\. Also, method signatures allow the use of attributes on parameters or return values\. How about that for local functions? Here's [C\# 9](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-9)—wish granted, answers Microsoft\.

But the language developers went further\. In their opinion, namespaces and the `Main` method scare beginners so much that they run off to learn Python \(unverified information\)\. So, in the same ninth version, it became possible to discard these horrors\. [Top\-level statements](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/program-structure/top-level-statements) allowed writing code as if we were inside the `Main` method existing somewhere out there\. And that's true, as the feature is nothing more than syntactic sugar\. With this approach, these are local functions introduced earlier that help declare methods\.

By the way, lambda functions are very similar to local ones\. They have some inconveniences compared to the latter, but still\. Local functions can declare optional parameters and use attributes, so why can't lambda functions do that? C\# versions [10](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10) and [12](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-12) fix these omissions\.

Now let's move on to the `ref` keyword, which allows passing an object to a method by reference\. Again, [C\# 7\.0](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-70) expands the horizons of what's allowed\. Now we can [declare variables with `ref`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/declarations#reference-variables)—creating an alias for another variable—and even [return a value by reference](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/jump-statements#ref-returns)\. Then comes more: [`ref readonly`](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#ref-readonly-parameters), [`ref struct`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct), [`ref-fields`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct#ref-fields), `scoped ref`\. We also have to teach the compiler [not to complain](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-13) about using these ref structs in generics or attempts to implement interfaces with such structures\.

An extensive layer of functionality relates to [`records`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record)\. Initially, the `record` keyword allowed quickly declaring a class with a set of properties, with all the boilerplate like `Equals` and `ToString` generated by the compiler\. But what if you want to use a `value type`, not a reference type? Meet `record struct`\. This combination of keywords \(and also `record class` as an alias for `record`\) appeared in [C\# 10](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10) and brought with it coupling with the `readonly` modifier, applicable to ordinary structures since [C\# 7\.2](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-72)\. As a result, we can now declare records in these ways:

```cpp
record A();
record class B();
record struct C();
readonly record struct D();
```

On [dev\.to](http://dev.to/), there is an [article](https://dev.to/waelhabbal/records-in-c-a-deep-dive-46hi) that includes code like this:

```cpp
public ref readonly record struct Point(double X, double Y);
```

This seems to be AI\-made, as such a construct won't compile\. Although its existence would fit perfectly into the trend shown above: `readonly ref struct` does exist, after all\.

At the same time, we can add methods and properties to records, override them, etc\. In other words, they are very similar to ordinary classes and structures\. It's just that the latter don't support that wonderful syntax where the type name is combined with the constructor\. Here, C\# history leads us to the concept of [primary constructors](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#primary-constructors), which appeared in the [12th](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-12) version of the language\. By the way, this [feature](https://github.com/dotnet/csharplang/issues/2691) is among the leaders in the number of negative reactions—186 votes "for" and 96 "against\."

<details>
   <summary>How I counted votes</summary>

1. I took the documents with new feature proposals from the [proposals](https://github.com/dotnet/csharplang/tree/main/proposals) folder in the [dotnet/csharplang](https://github.com/dotnet/csharplang) repository \(files in subfolders csharp\-<version\>\)\.
1. Inside the files, I found links to champion issues with the discussions\.
1. In each champion proposal, I counted user reactions \(_\+1_, _heart_, and _hooray_ are votes "for"; _\-1_ are votes "against"\)\. Of course, if one person put, for example, both _\+1_ and _heart_, only one "for" vote was added\.
1. Based on reactions for each issue, I calculated two parameters\. The first is a like ratio—the ratio of positive votes to the total number of votes\. The second is a dislike ratio—the ratio of negative votes to the total number of votes\. After sorting the proposals, I got two lists of each parameter's value in the descending order\.
1. Also, I created two sets with proposals ordered by the absolute number of "for" and "against" votes\.
1. After comparing the lists with each other, I got leaders and outsiders among all features\.
1. The method isn't very precise, but I couldn't think of another one\. Moreover, it's unknown what other emojis people might use and what meaning they ascribe to them\. Anyway, these are minor details that don't significantly change positions in the rankings\.


</details>


Anyway all of these are examples of useful innovations\. I myself occasionally use local functions and records\. It's just that I couldn't warm up to top\-level  statements\. It seems I'm not the only one, considering that Microsoft even added a checkbox _"Do not use top\-level statement_s" when creating a new project in Visual Studio\.

## When we need more keywords

An interesting story happened with [native sized integers](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/native-integers)\. Introduced in [C\# 9](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-9), the keywords `nint` and `nuint` were "enhanced" versions of `IntPtr` and `UIntPtr`\. The enhancement implied supporting arithmetic and implicit conversions between numeric types\. In other words, this code wouldn't compile:

```cpp
IntPtr a = 2;
IntPtr b = 3;
var c = a + b;
```

And neither would this code:

```cpp
IntPtr a = (IntPtr)2;
IntPtr b = (IntPtr)3;
var c = a + b;
```

But with `nint` the code would compile with no problem:

```cpp
nint a = 2;
nint b = 3;
var c = a + b;
```

However, in [C\# 11](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-11), Microsoft decided to add all these features to the long\-standing `IntPtr` and `UIntPtr`\. And if there's no difference, then let `nint` and `nuint` be just [aliases](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-11#numeric-intptr-and-uintptr) for the old types\. So, we have two keywords that add no value\.

Fun fact: they look like ordinary aliases for simple types \(like `int` or `byte`\), but we can't declare a variable named `int`, while creating a `nint` is possible:

```cpp
nint nint = 4;
```

It's because `nint` and `nuint` are [contextual keywords](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/#contextual-keywords)\. Moreover, if you don't know their history, you might wonder why exactly `IntPtr` and `UIntPtr` were honored to have aliases? Why not `TimeSpan`, for example? It appears even more often in most developers' lives I guess\.

There is another example of a sudden addition to the keyword list—and not by two, but by three at once\. It's about [pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching)\. This is undoubtedly a useful feature that significantly simplifies life\. Added in [C\# 7\.0](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-70), pattern matching has undergone many extensions and now represents a very powerful tool\.

In [C\# 9](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-9), new [logical patterns](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#logical-patterns) and new keywords were introduced: `not`, `and`, and `or`\. Just in case, I'll leave a [link](https://github.com/dotnet/csharplang/issues/1350#issuecomment-388638887) to a message explaining why common`!`, `&&`, and `||` couldn't be used\. Moreover, pattern matching is available not only in `switch` constructs but also in `if` statements by using the `is` operator\.

Although the logic behind adding new operators is clear, there's a feeling they are redundant\. Yet, I can't come up with a better alternative—criticizing without offering a solution\.

## When it's hard to guess

At times, Microsoft wisely tries to avoid adding new keywords and reuses existing ones\. The result of such care isn't always pleasing\.

It all started long before our designated starting point\. I think few programmers, upon first seeing the access modifier [`protected internal`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected-internal), guessed that the conjunction between these two words is not "and" but "or\." It seems quite reasonable to read it as "_accessible only from a derived class within the current assembly_\." But reality is cruel\. The correct interpretation is: "_Accessible from the current assembly \(from any class\) OR from a derived class \(from any assembly\)_\."

The cognitive dissonance and the need for an access modifier corresponding to the logical thought above led to the appearance of the no less strange [private protected](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/private-protected) in [C\# 7\.2](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-72)\. Who knows, perhaps in the future we'll see, for example, private protected internal\. Let the interpretation be: "_Accessible only from a derived class within the current namespace\._"

Another interesting object worth discussing is `default`\. The operator, introduced long ago, allows getting the default value for any type\. It's especially convenient for, for example, generics:

```cpp
public T Foo<T>()
{
    // ...
    return default(T);
}
```

[C\# 7\.1](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-71) simplifies the operator to a [literal](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/default#default-literal):

```cpp
public T Foo<T>()
{
    // ...
    return default;
}
```

At the same time, since ancient times, a parameterless constructor was called a default constructor\. And in both cases, the word `default` is used, so it seems both `default` and `new T()` should result in something identical\. Or not?

Let's declare a structure–a number that can't be less than 10:

```cpp
private struct A
{
    private int _x = 10;

    public A(int x)
    {
        if (x < 10)
            throw new ArgumentException("X is too small.", nameof(x));

        _x = x;
    }

    public override string ToString() => _x.ToString();
}
```

Next, let's create a default instance of this type in different ways:

```cpp
A a1 = default(A);
Console.WriteLine($"A1 = {a1}");

A a2 = default;
Console.WriteLine($"A2 = {a2}");

A a3 = new();
Console.WriteLine($"A3 = {a3}");

A a4 = Activator.CreateInstance<A>();
Console.WriteLine($"A4 = {a4}");
```

The console prints the following:

```cpp
A1 = 0
A2 = 0
A3 = 0
A4 = 0
```

Here we encounter an interesting feature of the `default` operator and literal: it ignores field initialization\. Attentive readers pointed this out to me in comments to another article\. Although, as with the example of `protected internal`, we can and should refer to the [documentation](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/default-values), the behavior still seems non\-obvious at first glance\.

Starting with [C\# 10](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-10), we can add a parameterless constructor to the structure, and the console output will change:

```cpp
A1 = 0
A2 = 0
A3 = 10
A4 = 10
```

This behavior caused controversy, and the [feature](https://github.com/dotnet/csharplang/issues/99) landed in the top ranks of the anti\-rating of innovations—40 positive votes and 29 negatives\.

It turns out that the word `default` isn't quite applicable to both the corresponding operator/literal and `new T()`\. Microsoft realized this too, so in the article ["Use constructors"](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-constructors) starting [April 20, 2019](https://github.com/dotnet/docs/blob/1828bf989338ffa3fa2af8c817c914c0cd32ace6/docs/csharp/programming-guide/classes-and-structs/using-constructors.md), the company no longer uses the term _default constructor_—now it's a _parameterless constructor_\. The last version of the text featuring the phrase _default constructor_ is dated [February 28, 2019](https://github.com/dotnet/docs/blob/d5f4a01db281ae2c5749219c0228714621f5b533/docs/csharp/programming-guide/classes-and-structs/using-constructors.md)\.

## Conclusion

Of course, there's more to grumble about\. For example, [default interface methods](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface#default-interface-members), introduced in [C\# 8](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-80)\. They caused an [uproar](https://github.com/dotnet/csharplang/issues/52)—89 dislikes against 142 "for" votes\. 

The language is expanding, and developers get the opportunity to solve their tasks a little \(and sometimes a lot\) faster and easier\. Still, sometimes there's a sense of lost elegance—something that made the language both simple and powerful\.