Hacker News new | ask | show | jobs
.NET (OK, C#) finally gets union types (andrewlock.net)
237 points by ingve 30 days ago
24 comments

I love C# and in every iteration we're getting more and more features to get C-like performance in a lot of scenarios. C# does it really well because if your problem isn't performance/memory-constrained, you can ignore these features and fallback on the language's natural ease of use.
I daylight as a .NET dev professionally. I completely agree with what you have wrote, but I do not think C# is particularly unique in that regard. I would say many common compiled languages are on the same path, e.g., Swift, Java, Kotlin, etc.. As time progresses, I am finding it harder to justify using C# for a greenfield project.
> As time progresses, I am finding it harder to justify using C# for a greenfield project.

Are you able to elaborate why? Just curious.

Not OP here - but for me it’s the open source ecosystem. Java just wins in terms of scope, scale, and stability.

I love C# the language, but the ecosystem is a ghetto.

I see this reason a lot but what are some actual examples of what is lacking in the .NET ecosystem vs. Java?
Large Apache ecosystem (Spark, Flink, Pinot) is completely missing and third-party SDKS (looking at you AWS) almost ALWAYS have worse SDKs. The java Kinesis consumer and producer libraries are amazing, the ones for C# are simple wrappers around the AWS APIs which means there's a few foot guns waiting for developers to run into, even if they should know better.
Dev tools. The debugger is something for example that Microsoft ostensibly keeps to their own products, and how they totally slaughtered omnisharp.

It killed my daily csharp vscode driver couple of years ago, only now catching back up somewhat, but still unusable for bigger solutions.

That move made me gravitate towards vscodium, and avoiding csharp where possible.

Microsoft's move only recently got more understandable to me, because Cursor and others basically stole vscode to establish their "empire".

Support for OpenAPI 3.1 in .NET sucks. Up until recently, a third-party library (Swashbuckle) has been the heavyweight, but lagged for years in supporting new features & fixing old bugs. Microsoft created a first party option (Microsoft.AspNetCore.OpenApi) that supports 3.1, but it’s nowhere near feature parity with Swashbuckle yet.

I also find serialization/deserialization to be weak in .NET. Third-party Newtonsoft was king for years, then Microsoft released System.Text.Json. Years later, it lacks feature parity, including an easy way to debug like Newtonsoft did.

Lots of implementations and industry standards, many mix Java with OpenJDK.

It is not, Java is like C, C++, JavaScript and co.

There are many implementations, the language and runtime are evolving by industry partners, you get bare metal implementations with real time GC like PTC and Aicas (doing AOT for decades), JIT caches, cloud based JIT compilers (OpenJ9 and Azul), pauseless GC, an LLVM like compiler development framework (Graal),....

There are industry standards like Microprofile and Jakarta EE, which several vendors base their frameworks and application servers on.

And a mobile phone platform, which while isn't proper Java compliant, has enough pieces into it that makes it easier to integrate Java code and libraries, than using Xamarin.

Microsoft actually bothered with ECMA during the early days, however it hasn't been updated since C# 7.3, .NET Framework 4.8.

Poor reporting libraries, PDF libraries, multiple popular open source projects have randomly gone proprietary (some after a decade+), etc..
I mean I haven't done much in c# recently, but few examples that c# ecosystem is the subpar in quality

Kafka client library sucks, I mean it was a nightmare to make it stable and there were a few of them.

Pdfbox library

And many other libraries. If you use c# Microsoft libraries only - then you are golden. outside of that its really bad.

At this point I switched to Rust.

A good built-in/open xslt3 processor.
GP here.

What I meant is that I try to be pragmatic when writing applications. Most languages solve most problems with relatively minor differences between them.

Of course, some languages excel in areas few or others do not. When it comes to C#, I struggle to justify when it is the “right tool for the right job.” If I were building a game in Unity, C# makes sense as it’s the only option. But if I were to build, I do not know, a FizzBuzz Widget, why use C# over any other language?

The only answer I can usually come up with is language familiarity. My employer’s answer to that question would be, “Because C# is a Microsoft product.”

Do you think by now C# has left Java behind in features and performance?
I never liked the C#/Java comparison - Java was designed as a much higher level language, and allows reasoning about low level abstractions a lot less, while doing a ton more optimization in the JIT.

For example, GC escape analysis, automatic lock elision, devirtualization, tiered compilation are fairly recent features in C#, and likely not as mature/powerful.

Or C# has generic specialization, so if your generic param is a stuct, you get a separate implementation, while Java generics work via type deletion.

But in C# you have a ton more synchronization primitives, value types, methods are non-virtual by default etc.

This usually means that expertly crafted C# code can be faster than Java (and more importantly, you can trust the compiler to do the right thing), while if you wrote it exactly like Java, you'd probably end up with slower code.

> while if you wrote it exactly like Java, you'd probably end up with slower code.

That's not the case for some time already, at worst you get similar performance with Java and with a little effort you can get significantly better performance.

C# was always better than Java as a language. The strength of Java is the ecosystem, and Java being open source and cross-platform from the beginning.
That used to be the case, but I would argue that C#'s ecosystem is just as competitive now. For our backend tech stack at work, all of our libraries used are open source nugets. Back 15 years ago, paid nugets were definitely more standard.
Language design isn't just about adding every possible feature. For example, someone mentioned operator overloading. As someone who has written a lot of Scala, I think operator overloading would be a very bad feature for an enterprise language like Java which needs to be consistent above all else. I never understood the obsession some people have with the C# vs Java debate anyway. Generally, both languages are very good at what they do, each having its own set of advantages and disadvantages. Regardless, a developer can pick up the other language with basically no effort.
> I think operator overloading would be a very bad feature for an enterprise language like Java which needs to be consistent above all else.

Operator overloading increases consistency. Instead of having

  int a = b + c;
  CustomNumberType x = y.add(z);
you have

  int a = b + c;
  CustomNumberType x = y + z;
Yeah it's bad when it's abused, so don't abuse it.

But not having operator overloading when dealing with vectors is maddening.

The problem with operator overloading is it makes things confusing when mixing types and let's programmers write confusing code.

    Person x;
    Job y;
    CustomType z = x + y;
WTF is Z?

Is the argument, anyway, I support operator overloading.

Java wasn’t open source at all from the beginning.
And the binaries, the default sdk when using java, i.e. oracle jdk, is still not open source, encumbered with a mine field of legalese. For a long time it also included malware as ask toolbar. Stay away.
No, but it was free/gratis distributed on almost every developer magazine shipping with CDs.

One of the reasons why it took off.

To the point that after all the drama with J++ that lead to .NET and C# development, Microsoft nowadays is also an OpenJDK contributor with their own distributions, and official developer advocacy channels on YouTube, conferences and devblogs.

Turns out they really want to have plenty of Java development on Azure as well.

Java Virtual Threads are a superior paradigm to C#'s async/await and function coloring.
I don't get what the big problem is with function coloring. You basically only need async when doing IO, and you had better know when a function does it, or you may have a bad surprise at some point in the future.
That! Okay, in 2026 this could be a LSP feature with some editor fanciness but this is a real benefit. My nitpick is that we do not have a depreciation on the non async ones
Agreed, async function coloring makes for better structured code because it incentivizes keeping IO code near the edges while having a synchronous core.
Clearly legacy heavy weight threads, virtual or not, are not superior. That’s why Swift, Rust and Typescript all chose async/await for concurrency.
We are not taking about legacy heavy weight OS/platform threads. But "green" threads managed by the language runtime like Go. Java went the Go/Erlang way.

https://javapro.io/2026/03/05/java-25-and-the-new-age-of-per...

https://docs.oracle.com/en/java/javase/26/core/virtual-threa...

> Clearly legacy heavy weight threads, virtual or not, are not superior.

Associating virtual threads with "legacy heavy weight threads" is a fundamental misunderstanding

> That’s why Swift, Rust and Typescript all chose async/await for concurrency.

And Java chose to join Team Go/Erlang. At the end of the day, async/await is just syntactic sugar for futures/promises, which are essentially a way out of callback hell.

Besides, Rust and Typescript aren't good examples here: a green-thread scheduler (a runtime component) contradicts Rust's philosophy, while Typescript is inherently constrained by Javascript.

Except one of the milestones for .NET 11 is to offer similar mechanisms for async/await.
Link?
Both are, IMO, inferior to Ada's TASK construct.
Hahahha no. lol just no.
It always has.
Only if we ignore that nowadays JVM does a better job at being the CLR, while Microsoft has forgotten what the C means.
more than 10 years ago, yes
More like 25. C# 1.0 already had capabilities Java developers are still dreaming of, like structs / value types, properties and operator overloading. C# 2.0 in 2005 introduced generics, implemented far more competently than Java ever did.
Notice you point out features that were easy to add during the beginning where there was no concern of backward compatibility? Now that java is porting value classes to mainline and we might have them soon, and planned operator overloading through type class, I believe java approach is making careful design choices that are semantically sound, rather than adding features that create edge cases such as boxing invariants in unions like C#.
So you try to say that Java gets to be more semantically sound by making bad choices early on? That does not make sense. Those choices are very difficult fix today and many of them can't be fixed. Say what you want but semantically more sound Java won't be. And the boxing with C# unions can and will be addressed later, this was a deliberate choice by the team to bring unions earlier.
The difference is BDFL or design by committee, a big difference between both ecosystems.
Well, what's the difference between a (benefactory or not) dictator or an oligarch?
Speaking as somebody that spent 2yrs as a full-time Java dev before returning to the Microsoft stack: yes.

Java’s Optional sucks compared to how C# (and Kotlin) implement support for nullable types. C#’s async/await syntax is better than… however the hell Java says to implement asynchronous calls now (Thread? CompletableFuture? idk, I never figured it out). ffs, Java doesn’t even have support for string templates yet — they added it as a JDK preview feature (JDK 21?) and then removed it before final release.

Damn. You're old school. Java’s answer is virtual threads, not async/await. The idea is that most server-side IO can stay in direct style enabling blocking-looking code, cheap virtual threads underneath. So you don’t split the whole codebase into sync vs async functions just to avoid blocking OS threads. CompletableFuture and reactive APIs still exist, but Loom reduces the need to use them as the default application model. You can now launch millions of virtual threads to do IO.
Not so much old school as I was new to JDK, there was no prior art anywhere in our codebase that implemented async (somehow, in 2022), and we started off with JDK 8 (I helped upgrade everything to JDK 17). I REALLY TRIED, OK!?

Even when I was building stuff in Kotlin, I couldn't figure out how to make async -- coroutines? RX? I forget already -- work, either. But that was all in the last few months before I left & moved back to the .NET stack.

Finally C# will have a more idiomatic way to represent this classic code example from “The Daily WTF” in 2005 —

    enum Bool 
    { 
        True, 
        False, 
        FileNotFound 
    };
See https://thedailywtf.com/articles/what_is_truth_0x3f_
What about Microsoft's own "MsoTrioState"? https://learn.microsoft.com/en-us/dotnet/api/microsoft.offic...

  enom MsoTrioState {
    Toggle,
    Mixed,
    True,
    False,
    CTrue
  };
Is it called "trio" state because 3 of the 5 states are not supported?

I also like how True is -1. Beautiful all around!

-1 means every single bit is 1, the truest possible value.
I’m having a surprisingly difficult time deciding if I strongly agree or disagree with this statement.
I know VB used -1 as the truth value, not sure about other languages.
Apparently it's sort of sensible, but weird.

msoTriStateMixed applies to aggregates. Eg, text.isBold() can be true, false, or a mix. Partly bold, partly not bold.

msoTriStateToggle isn't a real value but only used as a sort of flag. So eg, text.setBold(tristate), where "Mixed" would be invalid, and "Toggle" would flip the bold-ness of the text.

The msoCTrue one is where it gets really weird, no clue what's that for. I suppose an ill-conceived attempt to support the other way to express True.

True being -1 was a thing in Visual Basic and I suppose by some other Windows stuff. Logic being all the bits are 1.

note CTrue, the one true true
CTRUE IS A FALSE PROPHET AND ITS WORSHIPPERS SHALL BURN FOR THEIR HERESY
More languages need to support enoms.
Usual reminder that one of the hardest problems in software engineering is still naming things ;-)
Awful as it is, I can easily imagine how it happened... Probably says more about me than the code.
You're a true Romantic: Idealist and Pragmatist in one.
About 12 years ago I went to go and work on some legacy C++ software when I was very green. We had a big code base and it had enum Bool { False = 0, True = 1 } everywhere. I thought it was a good idea to rationalise this since we had conversions all over the place to the modern bool type.

So I suggested it, got a PR up (bit painful) it got reviewed, went in, everything worked fine, and we came about a week towards releasing the product (6 monthly releases) before someone noticed that we couldn’t load files from previous versions. Turned out that we wrote lots of these old Bool types to binary data files and so the 4 byte data was now being read as 1 byte data. Oops. Reverted the whole lot. Lesson in humility!

And as always when there is a mix of state and result, i post rop https://fsharpforfunandprofit.com/rop/
Could False = FileNotFound and this is a case of AST symbol constraints?
I've waited for union types on C# so long that I don't even care about syntax anymore. Just give us something that works. So, I appreciate the effort, I know it's taken at least a decade to get it into this shape, and much thought has gone into it. Kudos to the team.
F# has had this for decades, C# is basically just slowly becoming F# with a C-style syntax. Not complaining though, most teams aren't switching languages so getting these features where people actually work is better than nothing.
F# leads the way and C# slowly catches up, as always. Yet for some reason, C# still gets all the mindshare.
Haskell, OCaml, Erlang lead the way and Rust, Zig and Go get all the mindshare. I feel like its a common pattern for more experimental languages to pioneer features and other languages to copy the features and bring them to a C style syntax that the majority of devs are familiar with.
I wish I could find the reference, but there was a great blog / article by a computer science academic basically saying that OO, procedural, and functional paradigms are extremes of a design space where the “middle” of its Pareto frontier was essentially unknown until recent advances.

Moreover, many functional languages are getting pseudo-procedural features via the like of “do” syntax and monads, but that this is in some sense a double abstraction over the underlying machine that is already inherently procedural.

Starting from a language that is already procedural and sprinkling some functional abstractions on top is simpler to implement and easier for humans to use and understand.

Rust especially showed that many of the supposed advantages of functional languages are not their exclusive domain, such as sum types and a powerful type system.

Update: Hah! ChatGPT found it: https://news.ycombinator.com/item?id=21280429

Note the top comment especially, which explains succinctly why functional has rather substantial downsides.

Rust and Zig brought new ideas for memory management that Haskell, OCaml, Erlang sidestep having garbage control. its honestly amazing to me that they managed to get the adoption they have while being so innovative. I say this as a fulltime elixir dev.
What new ideas did Zig bring to memory management?
Pluggable allocators. Allocators are passed to functions as arguments, the caller decides exactly how memory is managed. Maybe not "new" in idea, but "new" in being consistently applied everywhere in language & libraries.
Already Turbo Pascal for MS-DOS had a custom allocator API for its runtime.

All modern C++ collection types have allocators as type parameters, and this was already a thing in the compiler frameworks like OWL during the 90's.

You would be surprised to see, how OCaml and the BEAM specifically think about memory.
Zig is Modula-2, Ada 83 with a curly brackets, what new ideas?
Being first isn't necessarily good if you get it wrong, though. Laziness by default and Hindley-Milner type inference seem like mistakes that simply aren't going to get cleaned up. Other languages make their own mistakes too.
What's wrong with Hindley-Milner?
Leaving out types in public API's makes type errors hard to understand. Types should be declared in the API and bidirectional type inference used in the implementation.

https://jimmyhmiller.com/easiest-way-to-build-type-checker

Eh. This causes some problems for rust. Right now you can have a function return impl Trait instead of a concrete type. Very handy - and essentially required by async functions.

But the language also requires that types have names in lots of places. For example, you can't store an 'impl Trait' in a struct. You can't make a type alias of an impl Trait. And so on. As a result, async rust can only interact with a butchered subset of the language. (You can work around this with Box<dyn Future<...>> but performance suffers.)

There's a proposal[1] to fix this. But the proposal has been under discussion for (checks watch) 7 years now. Until this lands, async remains a second class citizen in rust.

This entire problem stems from rust's early decision to requiring concrete types at interface boundaries.

https://github.com/rust-lang/rust/issues/63063

> I feel like its a common pattern for more experimental languages to pioneer features and other languages to copy the features and bring them to a C style syntax that the majority of devs are familiar with.

You're ignoring the fact that it's harder to bring new features to older languages as they have more bloat to deal with as not every idea turns into a success. So younger more focused languages can iterate more quickly. Also, being willing to make breaking changes makes things easier. Microsoft tries hard not to do that with C#.

Over time, maintaining any software becomes harder, languages are no exception. The fact that c# is still around, and still being developed is a feat in itself

The idea that Erlang is experimental is pretty amusing- it’s one of the most stable platforms there is.
I love F#. It's my go-to language and one that I work with every day. Personally I feel that IDE support (as in perf, QoL features, etc.) is the only area it lags behind C#, and outside of that it's a clear winner for everything that I want to do with it.
Microsoft management has decided CLR has a new meaning, C# Language Runtime.

VB, C++/CLI and F# are only there because existing customers.

They have always behaved as if it had been a mistake to promote F# from research project into VS 2010 as an official language.

Since then it has been something that the teams never knew how to sell to the .NET customer base, pivoting from being only libraries for C# and VB, write unit tests, Web development, data analysis, whatever might make it.

However it was Standard ML, Miranda, Hope, OCaml and Haskell that lead the way, we aren't still fully there.

> Microsoft management has decided CLR has a new meaning, C# Language Runtime. > VB, C++/CLI and F# are only there because existing customers.

Oh, I think we need a citation for these claims.

Can't think for yourself reading DevBlogs and Github issues?
What types of problems are better solved in F# than C#?

Is having a combination of F# and C# in a single codebase possible? Is it recommended?

Easy code is much easier in f#, a lot of the time. Hard code is usually easier in f# due to the type system helping a lot. F# is also a lot more concise.

And yes, you can combine them, but afair, only in terms project boundaries. (You can include a c# project in an f# one and vice versa). There are a few cases where it's quite useful. For example, rewriting a part of a big project in f# to leverage the imperative shell - functional core architecture. Like rewriting some part that does data processing in f#, so that you can test it easier/be more confident in correctness, while not doing a complete rewrite at once.

Sort of like rust parts in the linux kernel.

I don't think it's a matter of the type of problem and I always found it weird how F# is being framed as being only useful for "math-heavy" problems.

What matters is what libraries you are gonna use for your solution. If most of them are C#-only and don't have an F# equivalent then you'll lose the ergonomics and conveniences that make F# so easy to work with.

All types of problems. F# can do almost anything C# can do and with less ceremony. The quote I like is that once you get comfortable with F#, switching back to C# is like "having to fill out government forms in triplicate".
It's very possible, even encouraged when you have workloads that call for it. F# is a great functional language, so it's good for parsers, compilers, etc. The support for units of measure is also really cool, making it great for scientific computing.
C# gets all the mindshare because it's easier to understand and use on average.

We can all agree that F# is more clever and concise. No one is dying on that hill. But in terms of hacking your way through the customer requirements and working with a team of other humans, it cannot hold ground in the same way.

There is certainly not some concerted effort or lack of care involved. Microsoft could 10x the marketing budget around F# and the adoption rate probably wouldn't budge.

I hate the trend of measuring “easy to understand” based on how superficially similar the language is to C/Java.
That is entirely anecdotal and based on subjective interpretation of someone, who has worked in C like languages or something similar:

This is just about how you are used to it, it says nothing about the quality of the language itself.

Most people don't know that there are fundamentally two different kinds of "union" types: "tagged unions" and "untagged unions". Now .NET is introducing tagged unions, but unfortunately they stick to the popular tradition of calling them just "unions", which greatly adds to the confusion.

To clarify, these tagged unions are fundamentally different from the untagged unions that can be found in languages like Typescript or Scala 3.

Tagged unions (also called "discriminated unions" or "sum types") are algebraic data types. They act as wrappers, via special constructors, for two (or more) disjoint types and values. So a tagged union acts like a bag that has two (or more) labelled ("tagged") slots, where each slot has exactly one type and exactly one of these slot can take a value.

Untagged unions are set theoretic (rather than algebraic) data types. They don't require wrapping via a special constructor. They behave like the logical OR. They are not disjoint slots of a separate construct. A variable or function x with the type "Foo | Bar" can be of type Foo, or Bar, or both. To access a Foo method on x, one has to first perform a typecheck for Foo, otherwise the compiler will refuse the method call (since x might only have the type Bar which would produce an exception). If a variable is of type A, it is also of type A|B ("A or B"). There are also intersection types (A&B) which indicate that something has both types rather than at least one, and complement/negation types (~A indicates that something is of any type except A). Though the latter are not implemented in any major language so far.

These are not what are commonly called tagged unions. It's actually closer to an untagged union.

The C# union does not store any discriminator. Just look at the implementation - it's a single `object?` field.

The discriminative part is handled by run-time type information, which is stored in the object itself, not the union - which is why the C# built-in implementation requires boxing.

Also, you can use the same class/record type ("discriminator") in several different unions - again, a feature which ADTs/sum-types/tagged unions in most functional languages do not have.

You can even store one single object (ie. identical by reference equality) in several different union values at the same time, theoretically, which in combination with mutability is... uhh, certainly not common functional/mathematical semantics.

Underlyingly, all unions are the same concept, exposed by C's `union` keyword, namely overlapping memory (as opposed to `struct`s, which are sequential memory). You can add a discriminator tag to your unions, your call. If it is impossible for there to be ambiguity (e.g. you track the state somewhere else, and you're dealing with a large array), it may be redundant / memory inefficient to tag every item.

Having these tags be a language construct is just a DX feature on top of unions. A very handy one, but it doesn't make tagged and untagged unions spring from different theoretical universes. I enjoy the ADT / set-theoretic debate as much as the next PL nerd, but theory ought to conform to reality, not vice versa.

That's like saying Haskell is really an imperative language just because it's written in C. But it doesn't matter how it is implemented, it matters how it behaves on the surface.
> That's like saying Haskell is really an imperative language just because it's written in C

I honestly don't even remotely understand how you got to that from what I wrote.

Unions already have a definition. You cannot go around claiming that technically unions are not unions (and that "most people don't know" this), because theory X or Y overloads the term union to refer to some unrelated concept. That's fine and well and good, within that theory, but it does not displace the usual CS definition of union. It is a load-bearing definition.

A tagged union is so called because it is a union (overlapping memory) with a field (a tag) containing a discriminator (aka discriminated union). An untagged union is a union without this tag. These terms didn't come out of nowhere. They are transparent descriptors of what's happening.

It doesn't matter whether Haskell is implemented in C or Ruby, and it doesn't matter whether it's functional, object-oriented, or a DSL for modding Elden Ring, none of this changes what a union is. I'm very supportive of type theories exploring the option space for type constructs, but someone's going to have to implement these somehow. Such as with (tagged) unions. You know, that CS concept we have, with a settled and well understood meaning.

To be fair to you, I think you're letting a very narrow definition of 'union' shadow a much more usual and common definition of the word. And that's totally fine if that's the definition pertinent to you and your work, but if that's the case, maybe don't go around pontificating about how most people do not understand unions?

> You cannot go around claiming that 'technically' unions are not unions

I never said this. I said that there are two different kinds of unions with quite different definitions and behaviors.

> A tagged union is so called because it is a union (overlapping memory) with a field (a tag) containing a discriminator (aka discriminated union). An untagged union is a union without this tag.

That alone doesn't sufficiently describe the "unions" in set theoretic type systems like TypeScript. As I said, these unions are not disjoint and don't involve the special constructor that tagged unions/discriminated unions/sum types in algebraic type systems do. They also have logical implications like A implying A|B. Or A|A being equivalent to A, which isn't the case for discriminated/tagged unions. Maybe they are implemented similarly under the hood, but that doesn't negate the difference.

Edit: It's probably fair to say that "untagged unions" in languages like TypeScript, Ceylon or Scala 3 (set theoretic type systems) are different from "untagged unions" in C. It only adds to the confusion...

> Maybe they are implemented similarly under the hood

It's hard to know what to say to this. Tagged unions are not tagged unions, even if they are literal tagged unions? What?

I reiterate what I said in my previous comment, you're not using the ordinary definition of the term union, and this is causing confusion. A union may or may not be a "union" as understood within various academic type theories, that really depends on how any given theory defines that word, which can be any way it wants. But a union is a CS concept with a clearly understood meaning, and when used without added context to suggest it is to be interpreted in some theoretical way, it is understood in that ordinary way.

OP's article is clearly using 'union' to mean tagged unions - he even shows off their implementation, with a tag. The author assumes that his audience will understand what he's talking about when he uses the word union, and it's not causing anyone trouble in this comments section. The fact that alternative definitions within various theoretical paradigms is very nice, bless their hearts, but not really relevant.

You may prefer other definitions to the usual CS definition, that's certainly your prerogative, but - again - that's hardly grounds for taking an article and comment section that's using the commonplace meaning, and appearing to lecture others for failing to adhere to your idiosyncratic standards for what a union must be.

About time. TypeScript and Rust proved how much cleaner code gets when you can model "this OR that" at the type level. The real test will be whether library authors start using them in public APIs or if they stay a curiosity.
Standard ML proved this 50 years ago.
Why would the default language-level union keyword implementation force boxing? Seems like a crazy decision if it could just also implement the HasValue/TryGet itself and avoid it?
Hi there! C# language designer here, and one of the people working on unions.

Boxing is not something inherently to be avoided. It actually can work better in many (most?) use cases, and avoids a lot of problems that non-boxing approaches often cause (like tearing and copy costs).

It's try that the non boxing pattern could be implemented by us. And it's very reasonable that that is something we may do post this release. However, it's a non-trivial area. There's no one correct 'non-boxed' implementation. For example, do you have separate fields for all your unmanaged data? or do you have a blob of bytes that is large enough to align all your unmanaged data from teh largest set of of unmanaged fields, and you unsafe index into that?

Similar question for managed data. Do you have strongly typed fields for that data? Or do you attempt to use objects, to compact to as little space as possible? The former avoids casting costs. The latter allows you to minimize space. You can also potentially use unsafe casts. But those might introduce memory holes in tearing situations. etc. etc.

Because of this, i think the best outcome is to define the pattern (which we've done) and then use generators to allow you to control precisely the impl strategy, giving you all the bells and knobs you want to best fit your domain.

Can we expect that the C# development team works with the F# crowd, to make this copied feature work on both?
Yes. We do. And unions should work well in F#. It's designed to be a very easy pattern for all CLR languages and compilers to understand (including F#).
It would help if generators usability and related docs would be improved, though.

They still feel like something only .NET team masters, and a few MVPs that wrote key reference blog posts about them.

As a big user and fan of c# but this is a miss, as it always boxes value types.
It always boxes them TODAY. Lately the team has been releasing an MVP and improving stuff (like perf) later on. I wouldn't be surprised if they do the same thing here, as noted in the article you can already work around that yourself.
But why not do it right the first time. This is an obvious performance pitfall for people that want to adopt this feature. It is bizarre to me after the last decade has been dedicated to performance improvments.
They do this often actually. They will likely watch how people use it and tailor the design to fix that.

As for performance, in lots of use cases it's not going to be a big deal. If you are super sensitive to performance issues then you can just wait, meanwhile everyone else gets to use the new feature. You have to start somewhere and waiting to satisfy everything usually ends up with doing nothing

Hi there, one of the C# lang designers here. I discuss this a bit in https://news.ycombinator.com/item?id=48255658.

Contrary to what a lot of people guess, boxing is actually a really good strategy most of the time. And, is indeed what many people are doing here anyways. The design supports a pattern that allows for non-boxing, and I expect that we will both supply an implementation for that with reasonable defaults, and that source generators will be a great way to augment this to get highly specialized impl strategies for non-boxing depending on the varying domain needs any specialized customer may have.

There is the non boxing option, and this is the first iteration of the work, it's not one and done.
It is a downer, but I would like to test the performance first in the practical scenario. I've been working on a project where tagged unions would literally saved us from complexity. If that to happen again later I would go for boxed implementation and swallow up the penalty.
When I cared about C# (which is no longer the case), I was lightly involved in the discussion for this and sibling features - mostly theorycrafting exactly your ask: the JIT team very succinctly expressed extreme disinterest in adding support of any kind.

The C# compiler could do it to a degree, but there would be too many caveats to make it actually useful. Unless the JIT team has a change of heart, you're probably never going to see this.

What do you use these days language-wise?
I'm part of The Rust Evangelism Strikeforce. In all seriousness, whatever suits the task well. Rust is for passion projects where I want to enjoy coding something; I find Go to be more of a direct competitor to C# - but it has the edge due to lacking exceptions (which is 15 years wasted of my life, never again). I'm always searching for something better, though.
I'm glad to finally see this making it's way into C#. Not so much because I want to use unions purely in C#. But because I want to be able to define them when interfacing with other languages.
AFAICT, this means you won’t be able to define Either<string, string>, which is definitely a thing you sometimes want to do.
It seems like if you wrap both in a record then it should be possible:

    public record Left<T>(T Value);
    public record Right<T>(T Value);
    public union Either<L, R>(Left<L>, Right<R>);
Hi there. One of the C# lang designers here.

You're correct. The unions we're working on right now are 'type unions'. So the type is inherent in the union distinction, and you would not be able to distinguish that case. That said, we're also looking at full blown discriminated unions (you can look at one of my proposals for that here: https://github.com/dotnet/csharplang/blob/main/meetings/work...), which would allow for that. Syntax entirely tbd, but you'd do something like:

  enum struct Either<T1, T2> // or enum class
  {
     First(T1 value),
     Second(T2 value)
  }
We view these features as complimentary. Indeed, if you look at the extended enum proposal, you'll see it builds on top of unions and closed types (another proposal coming in the next version of the lang).
Thank you! Even what’s there is going to be really useful to me but I’ll continue to look forward to a full-fat implementation.
C# is strongly-typed, not stringly-typed. The point of the union is to list possible outcomes as defined through their respective types.

The idiomatic way to do this would be to parse, don't validate [1] each string into a relevant type with a record or record struct. If you just wanted to return two results of the same type, you'd wrap them in a named tuple or a record that represented the actual meaning.

[1] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

I guess C# is more strongly-typed than Haskell then... /s
String literal typing appears to be a common feature of type systems bolted onto dynamic languages:

    # Python
    MyStringBool = Literal("Yes") | Literal("No")

    // TypeScript
    type MyStringBool = "Yes" | "No"
I assume it exists to compensate for the previous lack of typing, and consequent likelihood of ersatz typing via strings.

It would seem pretty unnecessary in Haskell, where you can just define whatever types you want without involving strings at all:

    data MyBool = Yes | No
Of course you'd need a trivial parser, though this is probably a good idea for any string type:

    parseMyBool :: String -> MyBool 
    parseMyBool "Yes" = Yes
    parseMyBool "No" = No
    parseMyBool _ = error "..."
Interestingly, dynamic languages which make use of symbols (Ruby, Elixir, Common Lisp) probably fall closer to Haskell than Python or TS. Elixir example:

    @type my_bool() :: :yes | :no

    @spec parse_my_bool(String.t()) :: my_bool()
    def parse_my_bool("Yes"), do: :yes
    def parse_my_bool("No"), do: :no
    def parse_my_bool(_), do: throw("...")
Where :yes and :no are memory-efficient symbols, not strings.
String literals are structural types which are way more expressive than regular (Haskell) ADTs, which are nominal types.

In TS in particular, in combination with other features (mapped types), they are equivalent to row polymorphism + whatever Haskell/GHC features enable type families to specialize on constant literal arguments (or you can use atomic types, but that's not structural / open-world)... so pretty advanced.

This is valid TS/Python:

    type ABC = "A" |"B" | "C"
    type AB = "A" | "B"
    const x: AB = "A";
    const y: ABC = x;
The equivalent Haskell requires using several extensions.
I know. I literally gave the example of a Python Literal in the post you're replying to. TS too. :)

My overall point is that Haskell's type system is sufficiently expressive (you may not have "A" | "B" | "C", but you do have A | B | C) that there's no obvious remaining use case for string literals, unless you're thinking of typing input by way of expected literals instead of actually parsing it, which is... a choice. :P

Thank you for the lecture but I didn’t mean that. I meant `Either String String` is possible in Haskell and not in C# because… C# is strongly-typed.
You're welcome! Knowing is half the battle.

That Haskell snippet is just syntax sugar for Left(string) | Right(string), which is trivial in any language with unions.

Not clear why it would be an improvement over just naming the alternatives something meaningful, but if you're wedded to Left and Right, go for it.

You cant have a `type Foo = String | Strimg` in Haskell either.
But you can have an `Either String String` which is what GP was talking about.
My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.
You can use implicit operators or a library like Vogen to accomplish the same thing in a way that they can be coerced as strings. This isn’t a real issue.
Well it is a type union. The union of string and string is just string.
No, it's a union of a left value (that happens to be a string) and a right value (that happens to be a string). But the compiler-generated code can't tell them apart.
What you are describing is something different called a disjoint union which will maintain the identities of the left and right values when there is overlap.

The C# unions appear to behave like unions, not disjoint unions.

My mistake. I see my oversight now. `Either String String` is not equivalent to `String | String`, but to `Left String | Right String`. The same must be done for the C# version.

You are correct that this requires support for disjoint unions (aka tagged unions), which Haskell always had and C# will soon have.

but can you define T1 and T2 of string, then use Either<T1, T2>?
Could you be clearer about what you mean, since string is a sealed type in C#, so what exactly do you mean T1 and T2 of string?
A record wrapping a string, indicating what the string represents, so you can't mix it up with a different thing also represented by a string.
Yes, you can have two different record types which both wrap a string value.

As a (bad) trivial example, you could wrap reading a file in this kind of monstrosity:

    var fileResult = Helpers.ReadFile(@"c:\temp\test.txt");

    Console.WriteLine("Extracted:");
    Console.WriteLine(Helpers.ExtractString(fileResult));

    public record FileRead(string value);
    public record FileError(string value);
    public union FileResult(FileError, FileRead);

    public static class Helpers
    {
        public static FileResult ReadFile(string fileName)
        {
            try
            {
                var fileResult = System.IO.File.ReadAllText(fileName);
                return new FileRead(fileResult);
            }
            catch (Exception ex)
            {
                return new FileError(ex.Message);
            }
        }

        public static string ExtractString(FileResult result)
        {
            return result switch
            {
                FileError err => $"An Error occured: {err.value}",
                FileRead content => content.value,
                _ => throw new NotImplementedException()
            };
        }
    }

Now, such an example would be an odd way to do things ( particuarly because we're not actually avoiding the try/catch inside ), but you get the point. Both FileRead(string value) and FileError(string value) wrap strings in the same way, but are different record types, and the union FileResult ties them back together in a way where you can tell which you have.

It's more useful implemented a level deeper, so that the exception is never raised and caught, because exceptions aren't particularly cheap in .NET.

    type T1 string
    type T2 string
    type Meh Either<T1, T2>
Shame it'll be like 3 years until System.Text.Json sees support for serialization or exporting schemas for them..
Or being able to debug exceptions as easily as in Newtonsoft.Json.
C# is my strongest and favorite language. That said, it's frustrating that the C# framework ecosystem lacks solid options. MAUI is especially half-baked, and I'm really starting to doubt whether I should continue using XAML
C# used to be my favorite language, but having spent a lot of time in Rust using its algebraic data types + match statements + Option & Result types, then returning to C# to build a few moderately involved libraries, I'm horrified by the enums and null & error handling that I used to deal with all the time.

I knew that enums were really just named integer values and nothing more, but I had forgotten than you can build a perfectly legal enum from an integer out of the bounds of the enum's range. And a switch statement is non-exhaustive. (As I said, it had been a while since I used C# extensively.) What would have been a few lines of code in Rust turned into dozens to try to exhaustively protect against invalid input.

I know C# is a mature language that has been around for decades, but how janky everything feels comparatively really shocked me. I only very briefly played with F# about a decade ago, but my guess is that I could try to pick that up and call F# from C#, getting much better ergonomics with a combination of the two.

> I had forgotten than you can build a perfectly legal enum from an integer out of the bounds of the enum's range. And a switch statement is non-exhaustive

These are solved by the new feature described in the article that we're commenting on right now. They're giving us unions and exhaustive switch. Ctrl+F "canonical way to work with unions" in the article to see an example. One of the best parts about C# is they never stop bringing useful features from other languages back home to us in C#. It makes for a large language with a lot of features, but if we really want something, we'll eventually get it in C#.

And one of the worst is how long it takes them to implement even simple things. There are parts of the language (Expression) that are 20 years behind the rest and they don’t see the problem.
Learning Rust really ruined C# for me. The explicitness saves you from so much defensive programming.
Same. I wish there was a good story for .NET UI on desktop, mobile, and web. I find Maui Blazor Hybrid to be pretty close. The desktop and mobile variants compile to a webview bound to native code (no WASM, no JS), and the Web variant compiled to WASM. Much leaner than Electron. Pretty clunky though last time I tried though that was three years ago.
Winforms is better than ever. You can use it with .NET10 and WebView2 is a thing now.
Responsive UI, fluent styling, are they straightforward now, or do you still need to jump hoops?
WebView2 will cover all of these edges. If you are clever with web view environment and element reuse you can make the experience very smooth.
Winforms, wpf, blazor, maui, avalonia, what are you talking about
Alright. I'm actually fine with WinForms and WPF since my factory floor codes depend on them. But the reality is they aren't expressive enough for modern UIs. XAML is an issue, and WPF is boilerplate hell. But then Blazor is too heavy, MAUI is broken and buggy, Avalonia is underwhelming, and WinUI 3 is an absolute nightmare.
> But the reality is they aren't expressive enough for modern UIs

Other than web tech, what actually is expressive enough?

> Avalonia is underwhelming

How? Can you elaborate?

Not OP, but I've found Avalonia to be pretty much a direct replacement for WinForms. I mean that both as a compliment and a deserved insult. It's not the WinForms we wanted, but it is the one we deserve.

More seriously, it has all the strengths and weaknesses of WinForms and feels about exactly as unfinished and rough as WinForms. I still have to implement custom widgets that i would have expected to be included out of the box. It's nice that it's cross-platform, though with all the rough edges that cross-platform .net still has. It really, truly feels exactly like every C# UI framework I've ever used in the last 20 years: almost good, not quite finished, and takes an amount of effort that is just unreasonable compared to any other language/framework of any age.

I've been a C# dev for most of my career. I have more fun writing UIs from scratch by drawing individual pixels in C++ than any C# UI.

I'm truly surprised that it feels that underdeveloped. They market Avalonia as a direct replacement for WPF too. So, I'd expect it at least match WPF to be fair.
What I don't get is why Java doesn't get dogged for desktop UI like C# does.
Because Microsoft pushes C#/dotnet as the preferred way to write UI on Windows.
Which, for specifically Windows, it works perfectly fine. The only complaints I've heard is for a cross-platform UI framework.
Because people have given up that battle and just except that Java UI is crap.
Eto.forms too...
C# is such an underrated language.

A nice balance of language features and legibility.

Wow 2016 would have loved this news.
sadly there is no ad hoc way of defining them. I do not care if they are boxed. I just want to say Success or Error1 or Error 2. Maybe I can live with or | or other stuff but not having ad hoc unions is basically not cool.
Wow! I remember unions back in my Uni days. So many years ago.
How about F# compatability?
I wish the syntax looked more like typescript. This will confuse my eyes for a while.
It works quite differently from TypeScript, so the syntax used there wouldn't have worked.
If C# can get them then Golang can get them as well!

Come on Gophers! It's time.

Boxed, and needs complex incantations to avoid the boxing. Meh.
Hi there! One of the C# lang designers here. I discuss that here: https://news.ycombinator.com/item?id=48255658

Note that my expectation would be that the non-boxed form would be as trivial as adding `[NonBoxedUnion(SomeImplStrategyChoiceEnum)]` (or `[NonBoxedUnion]` for some default strategy choices that likely are ok).

This would give you extremely fine grained flexible choice on how you wanted your non-boxing union to work. There's no single right answer. There are just tradeoffs in terms of space/speed/copying-costs/memory-safety/etc.

I think it would make the most sense as people who care about boxing will have very different views and needs in terms of things like space, casting costs, copying speed etc.

The vast vast majority of users do not need to care at all. And for that, a boxed approach works exceptionally well.

I mean yes, but also: uh-oh. I'm looking forward to reading some code that is even more confusing than the code I'm already reading.

Not entirely convinced that I see the usecase that makes up for the potential madness.

This is a classic debate in programming, literally:

2001: "Beating the Averages" (Paul Graham) [1]

2006: "Can Your Programming Language Do This?" (Joel Spolsky) [2]

Both of these articles argue for the thesis that programmers that have been deprived of certain language features often argue that they don't need those features since they are already comfortable working around the lack of said features.

It's a fancy way of arguing: you don't know what you're missing because you've never had it. Or, don't knock it until you try it.

Consider, is your argument a) I've never used it and don't see a need for it, or b) I've used it before and didn't get any benefit?

1. https://paulgraham.com/avg.html?viewfullsite=1

2. https://www.joelonsoftware.com/2006/08/01/can-your-programmi...

I can already do functional programming like map/reduce in C# tho. Not sure what the LISP argument is. Spolsky was saying there's a perf benefit in there somewhere but I'm not seeing how unions give me that.
You have at least two options:

1. Argue from ignorance. Never try unions in any other programming languages and completely disallow their use in C# codebases that you participate in.

2. Try them out and adopt an informed opinion.

You may even choose to remain in ignorance until someone wastes their own time trying to convince you. But it isn't my job or desire to teach someone who won't put in the effort to learn for themselves.

its not your job to comment spitty replies either but yet you volunteer that time, when you could have been productive instead of whatever the fuck this shit is.

My primary concern with this pattern versus exceptions is calling code can simply discard the resulting problem.

I mean, they have to explicitly unpack the error and then choose to do nothing with it. It requires roughly the same amount of code to do the same with discarding an exception.

Except with a Result type the fact that an exception can occur and should be handled in the first place is explicit.

The problem if anything is that you MUST say something about the error case, despite the common scenario being “pass it forward” — the same reason exception do this by default. Which is also why rust for example special cases Result with the ? operator to do exactly that

Unions are simpler than subclasses and more powerful than enums, so the use cases are plentiful. This should reduce the proliferation of verbose class hierarchies in C#. Algebraic data types (i.e. records and unions) can usually express domain models much more succinctly than traditional OO.
> so the use cases are plentiful

such as?

> This should reduce the proliferation of verbose class hierarchies in C#

So just as an alternative for class hierarchies? I mean good people already balance that by having a preference for composition.

Simple example:

   type Expr =
       | Primitive of int
       | Addition of (Expr * Expr)
       | Subtraction of (Expr * Expr)
       | Negation of Expr
Isn't that just Func<int> ?
Really not. You can, of course, having instead a delegate to evaluate the expression. But then that's all you can do. You can't pretty-print it, for example, or optimize it, or whatever.
“Compoision”. A typo I know but it would be a word describing what goes wrong with class hierarchies.
Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.
> Discriminated union types are a really fundamental building block of a type system. It's a sad state of matters that many mainstream languages don't have them.

"Non-discriminated" unions (i.e. untagged unions) are even less supported. TypeScript seems to be the only really popular language that has them.

ok, so what problems do they help me solve that I can't already solve? Is it just that we can make code more concise or am I missing a trick somewhere?
I think "what problems do they solve that I can't already solve" is the wrong way to look at it. After all, ultimately most language features are just syntactic sugar - you could implement for loops with goto, but it would be a lot less pleasant. I think that unions aren't strictly necessary, but they are a very pleasant to use way of differentiating between different, but related, types of value.
Ok. I'm just trying to understand what code I'm replacing with them. Like I wanna see the before and after in order to gain the same level of excitment as other people seem to have for them.

Often the explanations just seem rather abstract which makes it harder to appreciate the win, versus the hideous sort of code that might appear when they're misused.

They are so fundamental to the way I write code I can't imagine ever using a language that does not support them.

"Make invalid states unrepresentable."

The value is realized when you have both discriminated union types _and_ language pattern matching (not regex). Then it's not just a way to structure data but a way to think about how to process it.
I think it's almost always about making code more concise and programming more ergonomic. Assembly could already solve all the problems higher-level languages can solve. Yet we didn't discard them as useless.
Object-oriented polymorphism (interfaces, inheritance) is for when you have a fixed set of methods to implement but an unbounded set of types that may want to implement them.

As a consumer, you cannot change the methods, but you can add a subtype. When you subtype an abstract class or an interface, the compiler does not let you proceed until you have implemented all the methods.

Discriminated unions are for the exact opposite situation, when you have a fixed set of subtypes, but unbounded set of methods to implement on them. As a consumer, you cannot add a subtype, but you can add a new method. When you write a new method, the compiler does not let you proceed until you have handled all the subtypes.

Good languages should support both!

The best example is abstract syntax trees, the data types that represent expressions and statements in a programming language. "Expression" breaks down into cases: integer literal, string literal, variable name, binary operations like add(expr1,expr2), unary operations like negate(expr), function call(functionName, exprs), etc.

Clearly all of these expression subtypes should belong to a base type `Expression`. But what methods do you put on `Expression`? If you're writing a compiler, you have to walk this syntax tree many times for very different purposes. First you might do a pass on it where you "de-sugar" syntax, then another pass where you type-check it and resolve names in the code, then another pass where you generate assembly code from it. Perhaps your compiler even supports different backends so you have a code-gen path for x86, another for ARM, etc. You'll likely want a pretty-printer so you can do automatic reformatting, maybe you want linting support, etc.

If you look at all those concerns and say that each subtype of `Expression` must implement methods for each one, then you end up with untenable code organization. Every expression subtype now has a huge stack of methods to implement all in one file, dealing with stuff from totally different layers of the compiler. It's a mess.

It's much cleaner to have the "shape" of the expression defined in one place without all that clutter, and then in each of those areas of the code you can write methods that consume expressions however they need, so each of those separate concerns lives in its own silo.

------------------------------------------------

Some real code (but it's F# not C#) to look at.

AST for my SQL dialect: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Typechecker code: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez... Backend code that outputs MS TSQL from it: https://github.com/fsprojects/Rezoom.SQL/blob/master/src/Rez...

------------------------------------------------

If you're an old hand at OO you may be familiar with its actual answer to this problem, the "Visitor" pattern. See System.Linq.Expressions.ExpressionVisitor. However, once you've used a language with good union and pattern matching support, this feels like a clunky hack. Basically the mirror image of a language without real object orientation imitating it by passing around closures and structs-of-closures.

------------------------------------------------

It doesn't just have to be compiler stuff. A business app data model can use this too. Instead of having:

    public class DbUser
    {
        public EmailAddress Email { get; set; }
        public PasswordHash? Password { get; set; } // null if they use SSO
        public SamlEntityProviderId? SamlProvider { get; set; } // null if they use password auth
    }

You could have:

    type UserAuth =
        | PasswordAuth of PasswordHash
        | SSOAuth of SamlIdentityProviderId
The implementation details of those different auth methods, the UI for them, etc. don't have to be part of the data model. We do have to model what "shapes" of data are acceptable, but "doing stuff" based on those shapes is another layer's problem.
thank you so much, that's extremely helpful <3.
Simple example that I use often when writing API clients:

In current C# I usually do something like

public class ApiResponse<T> { public T? Response { get; set; } public bool IsSuccessful { get; set; } public ErrorResponse Error { get; set; } }

This means I have to check that IsSuccessful is true (and/or that Response is not null). But more importantly, it means my imbecile coworkers who never read my documentation need to do so as well otherwise they're going to have a null reference exception in prod because they never actually test their garbage before pushing it to prod. And I get pulled into a 4 hour meeting to debug and solve the issue as a result.

With union types, I can return a union of the types T and ErrorResponse and save myself massive headaches.

I think I get it but I'm not really sure what I'm gaining over exception types. With an intelligent use of exceptions I can easily specify the happy path and all the error paths separately which seems really nice to me, because usually the behaviour between those two outcomes is rather different.
> I think I get it but I'm not really sure what I'm gaining over exception types. With an intelligent use of exceptions I can easily specify the happy path and all the error paths separately which seems really nice to me, ...

Until your coworker comes along and accidentally refactors the code to skip the exception catching and it suddenly blows up prod.

With tagged unions you can't accidentally dereference to the underlying value without checking if it's actually proper data first.

Exceptions are significantly slower than normal control flow in C# (about 10,000 times slower). It's also pretty non-idiomatic in both C# and most other languages I've worked in to use exceptions instead of a switch statement or similar to handle an HTTP error code. Also there can be multiple possible non-error responses from an endpoint you need to differentiate between, and exceptions would make zero sense in that case.
make the compiler check more stuff, helpful when expanding or refactoring
Union/sum types are generally a good thing. Even Java added them. They tend to be worth “the madness”. Now the rest of all the crazy C# features might be a different question.
What features do you see as crazy?
All the weird cruft around nullability, for starters. Once again confirming that allowing null references is usually a mistake.
Do you mean the implicit nullable types? Now that you can make nullable explicit instead I really don’t have much issues with it. It is part of the type system, as it should, and you have null coalescing operators. Is it still problematic or are you dealing with older codebases where you cannot set the nullable pragma?
Yes, all that stuff. I try to stick to F# where no special syntax is required for missing values (via Option<T>).
Maybe not crazy but the language just has a really broad surface. I find it to be like the Scala of the OO world.
Have you considered trying them out (maybe in F#) to understand why they are so popular in many other languages?
A common use case for the sum type is to define a Result (or Either) type. Now, C# not having checked exceptions is not as much in need for one as Java is, but I could still imagine it being useful for stream like constructs.
yeah this is the one I've considered as being mildly compelling. But don't we lose the fun of having exception handling as separate to the happy path?
oo and support for exceptions, in particular checked exceptions, was a mistake of the 90s. We know better today, there’s a reason for why modern languages like go/rust/swift don’t use them, and why many use c++ with exceptions disabled.
Checked exceptions are great. There is no difference between them and results/unions except syntax. The below all express the same thing. Swift in fact uses the checked throws syntax.

    Result<T,E> fn()

    T | E fn()

    T fn() throws E

    fn() throws(E) -> T
I've never been confused by language features. Usually the architecture or extreme indirection of the code is the confusing part.
Did Anders Hejlsberg die, or something?
He's been busy with the typescript-go project
I used to see some excitement around .net core several years ago. I haven’t heard or seen much in the wild. Is anyone using .net on systems other than windows nowadays?
It’s huge in the game dev world, with Unity and Godot. .net also had a reasonable community on mobile for a while thanks to Xamarin, but I cannot imagine that many people using it for new mobile projects in 2026 (outside of game dev I mean).

It’s a very decent language (I mean C#) and runtime, I wish it had more market share in the startup world.

Unity is still using Mono these days which is missing basically all of the C# and .NET improvements from the past... 10 years now?

Godot was using Mono too but has since switched to .NET in version 4.

Still a great language and I hope Unity can hit their target to switch to .NET soon!

Damn, I assumed they had switched to .net core, I cannot believe they are still stuck on mono. Thanks for the correction
An enterprise shop I co-op'd at was porting one of their apps from Xamarin to MAUI when I worked there, but certainly it doesn't have much mindshare (if any) amongst SE undergrads at my university.
Someone I know who works with .net says that there is still no replacement for full Visual Studio for development, which is Windows only.
Rider is the replacement, unless they are doing really specific (like WinUI2/UWP)
VS Code is also manageable. Or the CLI tools, if that's your thing.

Rider is definitely the most equivalent to full Visual Studio though.

I used to think this. Hopped to rider 4 years ago and haven't missed it except for .sqlproj development.
I consulted for multiple enterprise C# projects in the last 5 years. At least two of them are 1mil+ lines of code each.

All of them run in Linux servers.

Some of them were ported from PHP and Python to C#.

Plus LLMs thrive in strongly typed languages.

Which means C# will keep being very strong in enterprise too. Not only in games where it reigns a large chunk of the market share.

Yes; many (Alpine/Debian) containers in K8s on GKE for production rail ticketing infra in the UK.

There's not tons of noise being made because for the most part it all, Just Works and that's fairly boring. Perf, memory usage etc gets better every release. As an ecosystem, I'm pretty happy with it. I reach for other languages for smaller microservices.

> rail ticketing infra in the UK

You mean Raileasy? Or RDG too? (Just curious about the stack of the wider rail tech infra)

What's preventing you from using C# for smaller microservices? And what do you reach for?
Maybe startup time was a problem before AOT?
For almost 10 years now, we have not published anything .NET to the Windows platform. .NET is more performant on Linux today than Windows, and I would say development is also better there (using Rider). However, we do still have devs who prefer Windows. We have built many critical systems on .NET and they just work, so they may be boring to some of the folks who like to have more excitement from their systems.
Yes, lambda's and our dev's use mac's so it enables that. We deploy some apps to some unix based server as well but the company is mostly windows servers anyway.
Wwwuuuuuaaahhhhh! (making a big wild excited noise using asp.net core exclusively on Linux servers since 2017)
it was an obvious marketing campaign. back then core and blazor were shilled relentlessly, and the artificial excitement died the moment MS moved on to shill vscode and typescript.

companies spend a lot on marketing, and it's not just ads.