Is C# Underhyped?
old.reddit.com
old.reddit.com
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.
Ignoring the big issues, it's paper cut after paper cut.
It's infuriating after you've used a language with thoughtful ergonomics.
Yes it is. I've got it in my game right now. It totally supports fall-through.
See https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
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.
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.
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.
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.
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.
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).
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#.
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.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).
> 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.
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.
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.
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 :-)
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.
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.
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.
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.
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.
> 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 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.
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.
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.
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).
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.
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).
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.
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#.
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'
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.
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.
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.
VSC extension https://marketplace.visualstudio.com/items?itemName=ms-dotne...
Or if you prefer VS https://visualstudio.microsoft.com/vs/community/
Edit, also happy to provide stack traces from the ones I tried, it's not as portable as it wants to be.
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.
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 end up making glaring mistakes like using IEnumerable instead of IQuerable. Or making what is going to execute as 9 table joins in linq but not wonder why it takes 4 minutes to execute. Which because you can get away with some pretty bad CI/CD pipeline in C# (for a while) ends up going to production.