80 comments

[ 3.2 ms ] story [ 102 ms ] thread
Nulls, exceptions, and no discriminated unions. I use C# professionally and those traits completely ruin the language. And, no, the nullable stuff doesn't cut it. It has some pretty awkward quirks.

Also, almost nobody writes code that is fast. Sure, aspnetcore might be optimized to the teeth, but drop EF in and that's all wiped out and then, not some, a ton.

Idiomatic C# has also become what I call abstraction infatuation. It's impossible to tell what actually happens in that perfect 5 line method that uses 30 different interfaces and 10 different factories. Better have a good debugger handy (you won't: they all barf on async code). Aspnetcore and EF take abstraction to such academic levels that the most hardcore of Java EE devs would blush.

I started learning it months after it launched, I am a truly seasoned C# veteran. I thought it was the best thing since sliced bread. But hear-you-me, I have had one too many exception in my time, and C# is massively overated.

Have a look at the codebase for OHM (Open Hardware Monitor). It's fairly clean C#, and I found it refreshingly easy to grasp. I know what you mean about all the abstraction but you can use the language without all of that.
Good C# is certainly possible, XNA was a thing of beauty. There are still fundamental issues with the language. Another example (beyond nulls etc.): why do C# switch statements require break? Fall-through isn't supported, so it's just dangling there as an artifact of a previous language and not, oh I don't know, doing something useful like breaking out of the loop that contains the switch. When was the last time you took advantage of the variable scoping in switch, vs. when was the last time you had to figure out what to call a variable because of the scoping rules.

Ignoring the big issues, it's paper cut after paper cut.

It's infuriating after you've used a language with thoughtful ergonomics.

Blame C/C++ for the switch complaint. C#'s design was to avoid a lot of those nasty pitfalls by making you be explicit about your intent. You don't have to use a break, but you do have to explicitly exit out of the switch, so a return, or throw, or (gasp) goto works also. This means forgetting a break is a compiler error, not the silent execution of subsequent cases, which is probably a trick employed purposefully in C++ like 5% of the time and a nasty bug for every other instance.
> why do C# switch statements require break? Fall-through isn't supported

Yes it is. I've got it in my game right now. It totally supports fall-through.

... by using a goto case, which you can put anywhere in a case. The break remains non-functional.
Switch in C# is pretty much Rust's 'match' (aside from Rust having unit type and being expression oriented which gives it an advantage). It support list pattern matching, object type and member pattern matching, inline slice patterns, etc. You don't need to use switch statements for that either, switch expressions is the recommended syntax today and IDE is likely to even suggest to auto-fix some of the idioms it can recognize.

See https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

I remember C# being great a few years back when I used it. I am surprised that it doesn't have better discriminated unions since languages like TypeScript, and F# have them.

Is the interop between C# and F# not good? I've never used it myself, but I remember (and this is validated by a quick web search) that it's possible to just directly call F# code.

F# discriminated unions are always heap-allocated - the .Net runtime doesn't currently support address punning (having a single memory location possibly be a heap value or a stack value is UB). That may (always profile) put you at odds with writing fast code - and probably will: remember that significant portions of aspnetcore's recently skyrocketing performance have come down to eliminating allocations.
F# does have [<Struct>] DUs now but I believe because of the runtime limitations you mention, it ends up as sizeof(fields of case 1 + fields of case 2 + ...). They might reuse fields of the same type so if your DU cases can carry an A of string or B of string*int, the underlying struct is sizeof(string+int) not sizeof(string+string+int), but I'm not sure.

Regardless F#'s biggest downfall is that to use it you basically have to be a C# expert already and learn the F# syntax and additional features on top of that. Otherwise it's a baffling standalone language because you're left wondering why there are two ways to do almost everything -- Task vs Async, struct ("tup", "les") vs ("regular", "tuples"), two kinds of extension methods for classes, Nullable<T> vs Option<T>, it goes on and on.

TypeScript only has "discriminated unions" in an extremely busted form. While they are encoded in its type system, there is little support in control flow. Exhaustiveness checking is essentially optional: it's enabled in switch statements by either `assertNever` in the `default` case, or by limiting your function return from within switch cases with the `strictNullChecks` option enabled.

Without a match expression and first-class exhaustiveness checking, discriminated unions are far less useful. Of course, this is because they don't want the TypeScript compiler to actually generate code, so they can't add runtime features. While TypeScript is better than no TypeScript, it seems like a massive waste to build a compiler and then shy away from making people's lives easier when possible.

(comment deleted)
> it's enabled in switch statements by either `assertNever` in the `default` case, or by limiting your function return from within switch cases with the `strictNullChecks` option enabled.

What's wrong with using if statements?

```

if(shape.key === 'rectangle'){}

else if (shape.key === 'circle') {}

else {x: never = shape} // todo: use exhaustiveCheck(x: never)

```

That's pretty clear and easy.

[flagged]
I find Chrome JS debugging pretty awesome. What features it lacks so it’s utter trash?
> printf/console.log aren’t debugging, peasant

You might have missed the bit where I said the tools barf on async code - something a printf debugger wouldn't care about. It logically follows that I am indeed a user of a debugging tool, and not a "peasant". Though primitive tools like printf and memory dumps do have their advantages. Notably, printf is resilient in the face of async code.

You were also practically forced to use VS for C# 1.0, and by consequence a debugger.

VS loses track of where the async stack is (the parallel stacks view sometimes saves the day), the Rider debugger outright crashes and takes the process with it - and if it doesn't, completely fails to step into async methods.

I haven't found the async call stack in VS to be bad unless I'm doing stuff like firing off "async void" calls or firing off "Task.Run"s. It sure does make the stack trace on Exception.ToString() ugly but the call stack in the debugger is ordinarily quite navigable.

Now what can be annoying is when your problem is in some ASP.NET core middleware and you can't find it debugging your controller methods.

Also I don't love the "fluent API" configuration style they've adopted. I always have to Google what my chain of services.AddMvc(options => ...) and app.UseMvc(...) is supposed to look like. These APIs seem like they should be very discoverable through intellisense but since you can't "see" extension methods until you open their namespace, sometimes you have to already know exactly what you're looking for to find it. But then again, the old way of configuring a lot of those same things was via web.config and even less discoverable, so that's kind of a wash.

Edit: one other thing about the modern style of C# library design is the total rejection of statics in favor of dependency injection. Which I think is done with good intentions but somewhat overzealous. We no longer use HttpContext.Current, we have an IHttpContextAccessor that needs to be injected. We no longer use ConfigurationManager.AppSettings["foo"] we have an IConfiguration which again, needs to be injected. I get why this was done but I feel it trades convenience in the 99%-of-the-time case where those things really are quite reasonable to treat as static, for elegance in the 1% of cases where they are not.

(comment deleted)
I've never used a language without null, besides C# nullables (which I also find perfectly fine, though agreed to be somewhat awkward). What is the alternative and its benefit? Null fits so many issues perfectly naturally, like a database entry without a value. And I find the C# '?' syntax to be amazing. For those unfamiliar with it:

string foo = someDb.GetRow(id)?.Value

foo is null when the row is null or the value is null, with 0 null pointer exceptions possible. If you wanted to allow someDb to possible be null, instead of intentionally crashing out, you could also add another ? after that reference. Really wish C++ had this. But in general I find null pointer exceptions to be desirable, because more often than not they indicate a failure of assumptions.

Please go take a look at Rust and/or Swift to see how nice a language without null feels like. It’s not that there’s no way to model nullability/optionality. It’s just that there’s a much better way to do it. Simple, obvious, composable, no quirks.
I much prefer languages without Null (Rust being a personal favorite). I'll speak to that because I have a bit of experience with both C# w/ Nullable and Rust.

In Rust, you express types using the Option syntax. Its basically saying "This value could be None". It behaves similarly to nulls, but instead of Option being implicit (like C# nulls) its explicit. You can't do `let foo: String = None` because its an invalid type assertion. Whereas you can do `let foo: Option<String> = None`. In C#, you can't express "This is a string that cannot be null" (even with nullables if I recall correctly).

F# doesn’t have nulls by default and can completely interact with C# bybusi by Option types with the value set to None in case of null and Some<T> when it actually has a value.

This is so much more explicit and composes so much better than C#’s null.

If I declare a variable with a certain type in C#, its type is implicitly set to that type and null. I am allowed to call every operation that is supported by that type on the variable, and yet, it’s actually a lie because if the variable is set to null the purportedly supported operation won’t run and will throw an exception.

In F#, however, if I declare something of a certain type, it’s guaranteed to be of that type. Any operations on that variable supported by that type are guaranteed to run.

And if something might indeed not have a value (say a database result) I have to explicitly declare it as an option type so there is a None option for the value. This way the F# compiler will ensure I’m handling the None scenario properly and don’t end up calling unsupported operations on Null.

This is a much better design. I in fact struggle to understand why anyone thought it was a good idea to allow someone to declare a certain type but create an object which is not actually that type but is implicitly Type | null.

It makes sense in an unmanaged language like C, but makes no sense whatsoever in advanced languages like Java or C#.

Imagine if Nullable<T> allowed ref (class) types, and then disallow ref types from actually ever being null, and you have an approximation of the alternative: option/maybe types. https://en.wikipedia.org/wiki/Option_type

I absolutely did not get it until I really used it. It's 10x the sensation of fearlessness that C# non-nullable ref types give you. For example, you can't do something like this:

    var customers = new Customer[10];
(If I'm remembering correctly, C# in nullable mode allows this - and it's incorrect). You actually have to give it 10 instantiated Customer, or do:

    var customers = new Option<Customer>[10];
Then, once you've filled it:

    var filled_customers = customers.Select(x => x.Unwrap());
Now what's especially cool is that these are often treated like Lists with a maximum count of 1. What does that mean?

    var customer = new Nullable(new Customer());
    customer.Select(x => Console.WriteLine(x));
    customer = Nullable.Empty;
    customer.Select(x => Console.WriteLine(x));
That only does something in the first Select, the second is like an empty list. When you start thinking this way, you can do stuff like:

    customers.SelectMany(x => x);
So, keeping in mind what SelectMany does (it essentially flattens a list of lists into a single list), you'd filter out all the unallocated customers. Think about all the stuff you do with Linq (including the Linq syntax), and how nice your code would get if you could just treat null as a list of length 0. It's super-neat, ? on steroids (? does win in the brevity department though) and once you learn the mindset [generally good] code just flows out of you. It's like the Matrix, you have to see it for yourself to understand it. It teaches you a new way of thinking.
Sibling comments mentioned Option types, which are assumed the currently best approach. I agree, but would like to add Python as a different example.

Null handling in Python is actually sane, despite there being no Option type.

Its Null is called None, which is of type NoneType, which sits at the very top of the type hierarchy (inherits from object). That’s it. Everything else is separate from it. You perform a check for None, and after passing, you are guaranteed no null reference. Dereferencing cannot blow up.

A variable of type “Union of None and SomeClass” is safe to use after a None check. Type checkers will flag missing checks as errors. This is in contrast to C# or Java, where any reference type can end up in a rug pull and cause a null dereference.

In Python, assuming a type checker, checks are mandatory and cannot be forgotten (quite similar to Option types actually). In the other languages they’re not mandatory and can be forgotten (by default).

There's actually still plenty of insane things about None in Python. https://peps.python.org/pep-0661/ is a great example of that.

> Sibling comments mentioned Option types, which are assumed the currently best approach

Option types are actually just the tip of the iceberg though. The real power is in languages that support algebraic data types. Option is just one common example, used to distinguish between just Some and None, but it's reasonable to have an enum with other states, each optionally with its own associated data.

(comment deleted)
I don't like nulls and I definitely appreciate discriminated unions.

But WTF is wrong exceptions?! And by that I mean try/catch/finally.

I wouldn't use a language that doesn't have them.

The lack of exceptions in GO is just one of the reasons I hate it.

Go is a good example of what life would be without exceptions, code polluted with error handing.

Exceptions are invisible control flow. You have no guarantees about what piece of code will execute anywhere in your codebase. I agreed with you maybe 2-3 years ago but have since done a hard 180 in that opinion after seriously using a language with no exceptions. I'm too stupid to reason about exception control flow, just like I am too stupid to do manual memory management.

There are also huge differences between exception alternatives, and I definitely think more progress is possible. I also dislike how Go approaches exceptionless, but Rust does a far better job. Hare seems like it could have one of the best approaches (so far) but I haven't actually used it.

Erlang/Elixir/Gleam are also something I want to try out. Allowing things to just crash and restart is definitely a different perspective worth learning about.

Y'know what, I just took a look and I think I would like Rust-style error handling.

It reminded me about one thing that I dislike about exception handing in C#, that methods don't have to declare the exceptions that they throw, unlike Java.

And the thing I disliked about the Java way was declaring exceptions that would be panics in Rust.

I also like that ? Rust operator for propagating errors.

I've done some Erlang and I definitely like Erlang monitors.

Maybe the Rust way combined with monitors would be the ultimate in error handling :-)

What if there was syntax that was built into a language that required annotating exceptions functions that threw exceptions. That way at least you would know that an exception was possible to be thrown in a function and could handle it appropriately
Could you share the stuff that you're working with which make feel that the issues you mentioned are completely ruining the language? It sounds quite intriguing. I have mostly used in the last decade for basic JSON APIs and desktop application where I haven't really encountered those.
In my mind a Java is the main language that shares the same niche as C#, and I think for a long time C# came out ahead while Java stagnated, but in many ways I think Java has now pulled ahead imo.
(comment deleted)
OSS C# was a day late and a dollar short. Fundamentally, why would you ever pick it over Java? Theoretically it has a nicer IDE and some usability improvements, and better reflection. But that nice tooling was only ever available on Windows, recent revisions of Java have closed the usability gap, and people are finally realising what a bad idea reflection was (other than the Spring Boot nutters). Meanwhile library availability and general platform/tooling support for Java is orders of magnitude ahead.

It's kind of a shame, because in isolation C# is a pretty well-designed language. But people don't switch to better-designed languages, they switch to languages that have a killer feature that their previous language was missing. C# never managed that.

> OSS C# was a day late and a dollar short. Fundamentally, why would you ever pick it over Java?

While I do have major criticisms of C#, this is an often repeated criticism that doesn't hold much water. C# is Open Source (with capitals) regardless of how or when it happened, Oracle is notoriously litigious.

The main problem I have seen is that much like Swift or Cocoa, there are a bunch of Totally Not Open Source frameworks/libraries/etc that most applications for the original target platform use, and you must then dig into stack traces or hack code to avoid weird crashes because the code wants the Windows way of determining the local timezone, etc. I'm sure it could be great, I was a dot net developer many years ago, and it was fine, but it's a lot of work to make it work on Linux compared to the average jar. It's probably bad code vs bad language, but I've found other stuff easier to use or run, and avoid it as much as possible. Not comparing C, C++, Rust or Go because those don't even need a runtime installed, and I'm much more familiar with coding in those languages.
Neither needs C#. You can require runtime at a host, you can package it with your binary (which Go does, and C++ effectively too if you link statically).

But then again, Java can’t even do that. Its build and packaging systems are a tedious joke. If you want to package a C# binary, you simply invoke either of the two (one is JIT, one is AOT):

    dotnet publish -o {dest} -p:PublishSingleFile=true -p:PublishTrimmed=true
    dotnet publish -o {dest} -p:PublishAot=true
Done. No runtime required.

Also, most applications use ASP.NET Core and EF Core in back-end (which are platform-agnostic) and a selection of libraries for desktop and user facing applications on front-end: Avalonia, Uno, now MAUI, Xamarin (all of these are cross-platform) and historically WPF, Windows Forms, UWP and WinUI as their successor.

> Done. No runtime required.

But the part that matters is that you can ship a single file to people who are on any platform and be confident it will work. Yes the file is .jar and using a runtime, but those details are mostly irrelevant - everyone has Java installed somewhere.

Whereas with C#, sure you've got your nice binary, but if I try to run it on FreeBSD or Sparc is it going to work? From bitter experience, probably not - as GP said, probably for reasons that are more bad code and culture than a bad language, but the fact that several of those major (even if they're now considered "historic") GUI libraries are Windows-only and almost-but-not-quite work outside Windows sets the tone for the whole community culture.

> Sparc

> Presence of platform-specific GUI libraries makes different cross-platform libraries worse

> Supporting both self-contained and runtime-dependent lean binaries is bad

Hopefully this reply is a bait, otherwise I give the exercise in mental gymnastics a solid 7/10.

In Java-land, someone puts a jar on their website, and it will work on whatever OS, on whatever architecture, even if it's a GUI program.

In C#-land, that doesn't seem to be how it works. If it's a GUI program it'll probably depend on WinForms and fundamentally not work on official .net core for non-windows. It might work on Mono if you're lucky, but often it won't. And whether or not the program "could" work on your OS/architecture, most of the time what they'll put up on the website is a single-platform binary. Whatever the theoretical advantages, as someone just trying to actually use the thing on my computer, that's worse.

This almost makes me feel like we're back in 2007. Everything is so much simpler, my mother just finished cooking borscht and is calling me to kitchen. Life is good.
Oracle is indeed probably even less trustworthy than MS. But Java had a good enough release first, and a fully open source release first, and a decent crossplatform develompent story first.
Java doesn't even have "await" feature, while C# has it since more than 10 years.
Congratulation for spreading a disease in PL design. Await is controversial at best and should not be considered a pro.
Then why Swift, Rust, TS/JS and Python went with it as well?
Yes I literally called it a disease. Just look up “what color is your function”, I’m hardly the only person who thinks so.

There are more languages without async await that ones that have. The language that’s known most for massive concurrency, go, does not have it. None of the functional programming languages needs it either.

Oh no. Not the function coloring debate again (congratulations, with 1/100 of the talent, the author of that article is now inflicting similar damage as misquoting Donald Knuth on performance).

But if we have to do this, all functions in Go are colored but you simply can't do anything about it. Not allowing a method call to represent a delayed operation that produces a result (Promise/Future/Task) is the major mistake, and a reason why Go is surprisingly limited at concurrency even if it not seems that way if you come from a language which had even worse UX.

Why do you need to know what/what not is a delayed operation? Seem like the worse languages are ones you have to do that, instead of just caring about the computation. Polluting your function signatures with some abstraction that has nothing to do with the domain is pretty lame too.
"The group I belong to is obviously right and the other group is obviously wrong"

Forcibly hiding away the nature of the call shifts the priorities from easy concurrency especially when there are multiple calls in-flight to only logically sequential code being convenient to write with all other options requiring significantly more boilerplate (which is the case for both Go and Java).

It is a tradeoff, and I think it is a bad one for the domain that both Java and Go are usually applied to. Java had to retrofit a GT approach because async/await would be unfeasible at this point. It was a good choice in the context, but maybe not so if one were to reinvent it from the ground up.

On the other hand, Go took a similar route but for completely different reasons - it was specifically tailor made to first and foremost solve Google issues with internal dev culture and scaling of the organization (e.g. syntax styling being the function visibility modifier) and strictly prioritized being as quick to on-board as possible.

It resulted in Go being a language that is, as someone neatly put it here on HN, "Newbie friendly but professional unfriendly". This is further evidenced by the amount of surrounding code generation tooling and code written in DSL required to maintain an average complex project written in Golang (remember the HN post on a multi-million dollar project in dev hours to just work around Go's issues with nils?), something that is often a sign of inherent language limitations or its overall inadequacy (e.g. the criticism regarding C#'s source generators/interceptors or Java Lombok is valid because they exist for the same reason).

That's because Java has "java.util.concurrent" which kicks everybody's ass at concurrent programming.

Seriously. Any of the GC languages would be several orders of magnitude better simply by doing nothing but copying "java.util.concurrent" into their language.

Have you tried PLINQ? Much better than Java streams like LINQ which is a common knowledge but can easily parallelize arbitrary list comprehensions.
I've used C# quite a bit due to the Unity game engine. Moment to moment, it's pretty enjoyable to code in and easy to read. The cross-platform nature is usually really nice too (C# DLLs typically work on all platforms without issue).

However, I do have a few complaints:

We spend a lot of time fighting the garbage collector. This leads to awkward situations where certain variables that would ideally be function-local need to be made into member variables so we can reuse the memory each frame. This makes our code more complex and less local.

The choice to make "class" vs "struct" dictate whether memory is allocated on the heap or stack is annoying. I think there are a lot of problems with this: overloading keywords borrowed from C/C++, not being sure where memory will be allocated unless you go check the type definition, not having the option to instantiate a single type in both memory areas.

At first glance, you think C# doesn't have to deal with pointers. But in reality, just about everything IS a pointer, just with no syntax to indicate it. The fact that (almost) everything can be null makes code more complex.

I also find some of the syntax introduced in newer C# versions hard to read - but I guess I also feel that way about C++!

Ultimately, I still really enjoy/prefer C or C++. Despite their flaws, they give you a lot of options and a lot of control that I sometimes wish I had in C#.

You see issues with stack allocated variables in a function? There should be negligible cost to that.

The recent introduction of Span (or Memory for heap) has met most of my needs for fighting the GC and pseudo pointers without being 'unsafe'

> We spend a lot of time fighting the garbage collector.

Have you considered object pools? Getting objects into an older generation will eliminate their impact on regular collections. https://learn.microsoft.com/en-us/dotnet/api/microsoft.exten...

You want to be intentional about how long objects hang about. In C# game dev it starts becoming an architectural concern.

That being said, allocation problems aren't unique to managed languages. You'll definitely see at least one pool somewhere in a large-scale C++ game. It's always costly.

GC and nullability issues are an unfortunate result of Unity using forked flavour of Mono runtime (that is significantly behind feature-wise and uses Boehm GC). When Unity 6 comes out, all of these will hopefully become a thing of the past.

The larger .NET ecosystem does not have much issues with GC performance nor with nulls since most new code written is with enabled nullable reference types so you pretty much always know if a reference is nullable or not.

(comment deleted)
No, it's only useful on a shrinking platform.
What are you talking about?
I don't have a single mono/dotnet application or service installed on Linux, and I can't think of a popular one.

Edit, also happy to provide stack traces from the ones I tried, it's not as portable as it wants to be.

[flagged]
Let me add toxic developers to my list of complaints. I don't think this is a well reasoned response to my comment. Is this reddit now?

Edit: It's weird you called me a nazi, but I am not banned from Reddit, nor have ever supported the radical right. Please keep your ad-hominem attacks to other platforms, I am mainly on HN because it has less ridiculous noise like this.

They are a new account, seems to have been made just to trash talk/defend C#.
> I don't have a single mono/dotnet application or service installed on Linux, and I can't think of a popular one.

I don't have market share data, but dotnet is used a lot to develop ASP.NET Core services, which also target Linux and whose developer experience is delightful and provides top performance out of the box. I'd pick ASP.NET Core on Linux over any Java-based framework any day of the week.

Radar and sonarr are .net projects that work alright on linux
I have used some C# with Unity and Mono, so I can't comment too much on C# on the server compared to Java, Go, etc. It's an okay language but isn't bad.

That said, F# is a fantastic language. F# is Ocaml but a bit more simpler, better concurrency, and on the C# runtime (or to javascript with Fable).

Honestly, this thread is kinda low quality. OP seems to be taking criticism of C# personally and going full defensive mode. And the question itself is too vague to answer properly. Is this post just seeking validation of one's own preferred language?
I understand the meaning of the question but "hype" isn't going to encourage the best answers.
Swift, C#, Scala: the top languages for having a bloated design and make people choose between a gazillian ways of doing things for every single coding decision.
No. C# is not Rust and therefore is not relevant. Microsoft is rewriting core Windows components in Rust, not C#.
So Rust is better than C# for systems or OS programming… I don’t think anyone is going to argue with you on that.
Rust is also better than C# for fullstack web development. I don't give a remote s** if anyone wants to argue with me on that.
I worked with C# for a decade, and I think it’s never been better than it is now. I also think it’s a language that’s mostly meant to be sold by Microsoft in 2023 where it’s sort of outlived its usefulness compared to something like Go, or even Java.

I live in a very Microsoft happy region of the world, but not a single company that’s managed actual growth is using C#, and the language sort of lives in this “stagnated semi-large company that’s not going anywhere and hasn’t for a long time” space. Which might not have anything to do with C#, but more that the choices over all at those companies (which includes going too heavy in on the Microsoft tech stack) seem to hinder them from growing beyond 100-300 employees. Which on one hand is unimportant for the developers who work there, but often leads to a lot of those developers to “stagnate” as well, which sometimes lead to them having a rough time if they for some reason have to enter the job market at 50+.

So in many ways I think C# is very overhyped. Mostly because, why would you ever chose it? I don’t think it really compared with Rust. Rust can compete in the same space as C#, but you’d mostly pick Rust instead of C++, and I see things like Java, Python, Go and Typescript as its main competitors. Java mainly being there because large “boring” enterprise organisations like banks are going to use Java until all of us retire. But if you’d chose between Python, Typescript, Go and C# you just wouldn’t ever pick C#. Even against Java, I’d question why you would ever pick C#? Java is so far ahead of C# on concurrency that it’s painful to watch how C# developers are still stuck using things like await.

Yes it’s faster than Typescript and Python, but with Python being capable of powering the web-back ends at Instagram and OpenAI that is sort of a silly point. And Python is just soooo much more efficient in terms of producing things fast. It’s also telling that even Microsoft’s own C# developers pick Python over C# for things like prototyping and smaller applications they need to build fast. Go is king of concurrency and Microservices and you can see stories like Lunar who’ve gone from a Node backend to a Java backend to Go and now can’t imagine not using Go. Then there is Typescript, which today is basically what C# wants to be, and probably will be as the two languages influence each other more and more, but allows you to “full stack” in one language (except for the parts that run on Python and C++/Rust but you’ll have those regardless as some things require the efficiency of C++/Rust and the ML/BI/AI crowd aren’t going away from Python until the moon leaves our orbit). With companies like LEGO running most of their software on Typescript and Node, leaving C# behind, it’s also hard to talk too much about its inefficiency.

You can also look in my history and see my ranting about a range of the issues you’re eventually going to run into with the “magic” C# comes with, because a lot of their automatic handling of complexity can break in various ways when you need to use it in ways Microsoft hasn’t build it to support, and sometimes that’s simply using two different Microsoft libraries for C#. Which has nothing to do with the language itself, but let’s not pretend you aren’t going to use EF, Blazor, WebApi or similar if you use C# so it does impact you even if it has nothing to do with the language.

I don’t think there is anything wrong with C# though, it’s just that there isn’t really anything “right” with it either. It’s a language that doesn’t do anything other languages doesn’t do better, which includes being a jack-of-all-trades language where it’s also flat out losing to Typescript. If that last statement makes little sense to you, I fully understand, because a good Node development requires and opinionated CI/CD environment where as C# kind of doesn’t. But this is partly also why you’ll find so many C# developers get trapped by the language and...

It's a good language, better and safer than C/C++.

However, Microsoft has messed it up in multiple ways:

- It was great for Windows desktop applications, then they came out with Silverlight which then got abandoned.

- It has its own version of DLL hell. Users almost always need to download the .NET runtime. Windows 10 originally shipped without the most popular version, which made the runtime the most popular download.

- MS could not come up with a consistent naming and numbering conventions for their versions, Core, Framework and now just .NET

- Visual Studio Code doesn't properly support it, yes there are plugins, but to do anything useful you need Visual Studio proper.

(comment deleted)
Yes, it is. The comment section here is the proof of the overarching problem. People just don’t want nice things and would rather act on their tribal stereotypes instead.

Read through - you will see developers who unfortunately use either outdated platform that is Unity (it uses Boehm GC, in 2023!) until Unity 6 comes around. Or developers who don’t use C# at all, or the ones that had bad experience with some non-corelib library back in 2016 and then go onto crusade.

There are some for whom the actual state of affairs does not matter at all, and no amount of improvement throughout the years is sufficient.

I never touched it myself, because it's 'Microsoft's version of Java', so double-plus-ungood.

But my 14yo son has been trying all the languages the last year or so (from lisps to lua to rust, to C, to icon, vale, to, etc, etc), and out of all of them he's most impressed by C#. According to him, it's just really well designed, all just fits together beautifully ... (his opinion on Rust - it's just not complete yet).

So I think C# probably is under-hyped, and I'll be taking a look myself. That's if I can even download it; I guess I'll have to wrestle through a microsoft account login, app store, or family permissions, or whatever other user hell they've concocted lately.

When I was that age I had a high opinion of C# too. Over the years less so. Your statement relating to Microsoft sums it up well. It's overhyped.
Well, for a C-based language that needs to keep compatibility, I'd say it's one of the best. I'd love to see a breaking change (under a pragma or something) in order to have public modifier as default, everything as expressions instead of statements and so on -- get closer to Kotlin.