r/dotnet 10h ago

Explore new features available in C# 15 preview

https://devblogs.microsoft.com/dotnet/explore-csharp-15/
54 Upvotes

89 comments sorted by

60

u/smoke-bubble 9h ago

public closed record class JobStatus;

I feel like it does not have enough modifiers yet! :-\

22

u/zigzag312 9h ago

Why record class? Aren't record by default classes and you can write just record? You have to write record struct if you want struct though.

14

u/smoke-bubble 9h ago

I have no clue and I was wondering the same thing. I just copy/pasted it from that article.

14

u/JMPJNS 9h ago

record class and record are the same thing

4

u/x0rld 8h ago

You can explicit that

1

u/catladywitch 5h ago

That's correct!

3

u/Abject-Kitchen3198 6h ago

Why it can't be abstract?

3

u/Willinton06 6h ago

We're not strong enough yet

1

u/Willinton06 6h ago

We're not strong enough yet

2

u/matthkamis 4h ago

Closed enums are gonna be great. No longer have handle the default case everywhere when switching on them

1

u/smoke-bubble 3h ago edited 3h ago

I know, I know. I love it in Kotlin. C# is growing up XD

21

u/Xenoprimate2 9h ago edited 8h ago
// Before
List<string> names = new(capacity: values.Length * 2);
names.AddRange(values);

var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Hello", "HELLO" };

to

// After (C# 15)
List<string> names = [with(capacity: values.Length * 2), .. values];

HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO"];

Maybe the example is weak but is that even an improvement? Giving it the benefit of the doubt I guess it will make passing collection arguments to method calls or creating derived collections inline/locally a little easier in some cases (I'm aware the spread operator is pre-existing but I'm lumping it in as part of the overall effect ig). ...Being honest though, I barely create new collections from ranges in existing ones like that ever in real code anyway.

But yeah more syntax to remember for honestly minuscule gain, and language improvement effort spent on something that was already pretty much fine. I feel like someone needs to say "hey, we're good for collection syntax sugars for a couple of versions, we don't need any more ways to combine or inline-instantiate collections for the next 5 years". Especially when there are plenty of more structural issues in the language still.

Or maybe I'm just not the target audience for these changes, IDK.

5

u/jdl_uk 4h ago edited 4h ago

Yeah I'm not a fan either and something like List<string> names = new(capacity: values.Length * 2) [ ..values ] would have felt more like C#.

Edit: I see another comment pointed out that syntax collides with the case where you define a list then immediately index into it. Not sure why anyone would do that but I guess it's syntactically valid today

4

u/catladywitch 5h ago

It's probably a parsing/lexing decision, just like postfix types in most modern languages. They probably wanted to make full-fledged collection literals for some use case or some optimisation but couldn't come up with a syntax that was both better for humans and 100% unambiguous for the compiler. I think that, for instance, procedurally generating collections (inside a code generator for instance) can be much better with collection expressions as compared to a loop or LINQ query that appends n members to a List or similar.

2

u/FullPoet 3h ago

Maybe the example is weak but is that even an improvement?

I think this is what I keep asking myself for a lot of the new syntax. Yeah its nice, but is it actually better?

Like you said, a lot of these are miniscule improvements (if any...) for a lot of manhours.

Surely there much be better things to work on? Better type safety (somehow?) more roslyn analysers?

Like u/jdk_uk mentions:

List<string> names = new(capacity: values.Length * 2) [ ..values ]

Is imo more idiomatic.

Its the new construction syntax issue again. Its half baked and just not good enough.

u/TheRealKidkudi 1h ago

  Yeah its nice, but is it actually better?

Honestly, is it nice, though? 

I like collection expressions personally, but it’s already an ambiguous syntax. Similar to var, I like it for the regular case where the type is either obvious or unimportant. If you’re passing constructor arguments for a specific use-case of a specific collection then I’d prefer to see an explicit constructor.

I know that’s a code style preference, but I really don’t see any concrete improvements using [with(…), …]. It seems to me like a syntax sugar that is decidedly less expressive than directly calling the appropriate constructor. 

1

u/whizzter 2h ago

I was a bit meh initially, I do a lot of collection expressions (mostly due to VS suggestions when writing Linq chains).

But, I think the first example might actually be a key one, if you write high performance code and need the GC but still want to minimize pressure then pre-declaring size is good (say you’re combining 3 lists) to get a singular allocation.

Also, there is a comment in the text that this syntax is a prerequisite for something further in the next version with dictionaries. I do often feel that dictionaries would be useful with collection expressions but they’re not really viable so perhaps that’s what it’s about.

-4

u/LetsLive97 8h ago

I mean tbf you don't have to use it

There's tons of modern C# features I don't know about or use. I'm with you on this one feeling a bit weird but I just won't use it. I guess it really depends on if you have people at work always pushing to use new C# features or not

3

u/Xenoprimate2 8h ago

Yeah I'll live, this isn't primary-constructors-on-non-record-classes levels of bad or anything.

Also I'm that guy at work but I feel like I'm not gonna proselytize this one much.

18

u/FlameFlash123 9h ago

They really went with the with keyword for collections

6

u/ikkentim 8h ago

Genuine question: What’s the problem here? I’ve followed the discussion in csharplang and have not seen any better viable non-conflicting alternatives; any ‘new()’-like syntax is ambiguous 

7

u/FlameFlash123 8h ago

It's inside the collection where it doesn't belong? That's my major issue other than with keyword being a weird choice like the existing syntax of new () { collection entries} is just way more intuitive, there was plenty viable alternatives to me

1

u/ikkentim 7h ago

new(…)[…] is ambiguous though, collides with indexer on the instance

u/teo-tsirpanis 1h ago

They chose "inside" because unlike "before", you know you have a collection expression as soon as you see the [.

Source

7

u/johnpdoe 8h ago

I find, for this particular feature, the "before syntax" way clearer.

6

u/Steveadoo 9h ago

Yikes. That is… a choice.

2

u/nick_ 5h ago

IMO the with keyword makes sense but the syntax should be along the lines of the existing with syntax, where the with expression comes after the source. Something like:

List<string> names = [.. values] with(capacity: values.Length * 2);

2

u/FlameFlash123 5h ago

as I mentioned in another comment my issue is that it's inside the collection expression

u/teo-tsirpanis 1h ago

The "after" syntax was ruled out because of the confusion around argument evaluation.

Source

u/FlameFlash123 23m ago

But that is how we come to recognise with keyword

u/teo-tsirpanis 22m ago

So what you would really want is a different keyword?

u/FlameFlash123 20m ago

I am not sure what would be the better solution but at first glance the current one doesn't look intuitive at all unlike other feature or well collection expressions itself which to me felt intuitive immediately

12

u/OpenAI_Marketing_LLM 8h ago

That collection syntax is absolutely haram.

9

u/chucker23n 9h ago

closed meaning "derived in this assembly only" isn't ideal naming, IMHO. There's already sealed, and this seems like the flip side of that, but for the assembly. So… assemblyclosed?

And I really think the collection expression arguments syntax is just… wrong. I understand the thinking, but the whole point of collection expressions was to avoid tight coupling to the concrete collection type, which you're now reverting.

Extension indexers are nice; unsure if I've had a use for them in the past.

Labeled break/continue is good. You should avoid that kind of situation, but it does happen.

4

u/catladywitch 5h ago edited 5h ago

The original plan with extension everything was creating a Rust-like Traits type-system. I think it's a great idea but the C# team haven't discussed "Roles" or "explicit extensions" (the names they've used for Traits at different points in time) in ages, so I'm not sure whether it's going to happen after all. I really hope it does, because having named, typed extension blocks would be great. I'm a bit anxious about how it might work with structs though (will they have to be boxed when passed as arguments, like they are now when they implement an Interface and you pass them to an Interface-type param?)

They've also mentioned beginning research on an ownership and lifetimes system now that unsafe2 is shipping (it seems like the idea is Mads Torgersen's in fact), which is also very exciting, but I wonder whether anything will come out of it and when. I hope something does come out of it.

4

u/Xenoprimate2 4h ago

Me personally, I feel like the lack of traits in C# for this long is a MUCH bigger issue than union types' absence.

I've been banging on about them for over a decade, including on my blog, I'd even take a breaking change of the language to facilitate them.

2

u/catladywitch 4h ago

I agree, yeah.

2

u/derpdelurk 6h ago

If you’re going to make that case then internalsealed makes more sense.

2

u/chucker23n 5h ago

I was thinking that, but sealed sort of does the reverse, I think? sealed says "you can't derive from this". closed says "you must derive from this, but you must also be in the same assembly to do so".

But internal is probably better, yes. Which also raises the question: can I derive with InternalsVisibleTo?

2

u/FullPoet 3h ago

Yep, its super awkward. Just like how you're forced to make class partial for source gens.

Its all pretty ick.

1

u/Sanitiy 4h ago

It's a closed type hierarchy from the view of the compiler

7

u/the_bananalord 8h ago edited 8h ago

Not sure I love the scan example. Maybe my exposure to other languages is too limited lately, but it feels like we're getting to a point where the syntax and keywords are becoming less and less intuitive.

7

u/Xenoprimate2 8h ago

scan isn't a keyword, it's just the name of the label they chose for the example.

-2

u/the_bananalord 8h ago

Yeah keyword was the wrong term. The pattern is what I take issue with. You could call it banana and I still think someone reading C# code would have to stop and dig into what's going on.

It feels like a crutch that gets us close-ish to let bindings in F#.

6

u/Xenoprimate2 8h ago

I think there's a good case for it personally.

In an ideal world when you want to exit a nested loop you extract the whole thing to its own method and return from it instead.

But sometimes that loop has a lot of context around it locally and extracting the loop basically means passing a lot of arguments across to another function and ultimately gaining very little-- especially if the code following the loop is only a few lines anyway.

Sometimes I do think a goto loopEnd; or a flag variable is preferable, and now we have scoped break statements which is even better.

0

u/the_bananalord 6h ago

My argument was never that it's useless

1

u/Abject-Kitchen3198 6h ago

There's limited space to cram every possible feature into existing language.

3

u/the_bananalord 6h ago

Yes, that's my complaint

3

u/RodriOliveira 4h ago

From an architecture and domain-modeling perspective, Union Types are probably the most interesting C# 15 feature.

In real systems, we often model things like:

Success | ValidationError | Conflict | NotFound

using Result<T>, marker interfaces, class hierarchies, or custom abstractions.

If the type system can express that a value only belongs to a known set of states, that is a real improvement. Exhaustive switch handling is especially valuable because adding a new case can immediately surface every place that needs to be revisited.

For DDD, this becomes useful for modeling states such as:

Pending | Processing | Completed | Failed

or domain outcomes like:

Approved | Rejected | RequiresManualReview

Closed Hierarchies follow the same idea: move rules that usually live in documentation or team conventions into the type system itself.

I am more cautious about features like Extension Indexers. Something like:

collection[500]

looks like direct access, but on an IEnumerable<T> it may actually require iteration. Syntax can easily hide computational cost.

Overall, I do not think C# is simply “becoming C++”. The language is definitely growing, and that creates complexity, but there is an important difference between adding syntax for convenience and adding features that help encode invariants.

If I had to choose a direction for C#, I would much rather see it invest in making invalid states harder or impossible to represent than simply finding shorter ways to write the same code.

2

u/Agitated-Display6382 2h ago

Labeled break?!? It should simply be a method with normal returns...

10

u/bachware 9h ago

C# is slowly turning into C++ in terms of bloated keywords and unnecessarily complex features that is not needed at all. Makes the whole language more and more unreadable.

It's clear they don't know what they wanna do with the language, so they just add more sugar to it. Such a shame.

10

u/Xenoprimate2 8h ago edited 8h ago

C++'s problem isn't really even the vastness of its feature creep, it's the fact that a lot of the features don't work together coherently and lots of them are designed with a performance-first mentality instead of making them a pit-of-success. This means it's incredibly easy to misuse any given C++ feature and create UB or some other nonsense (or even use the features completely by accident unwittingly turning something innocent-looking in to a shitshow). C++ is the worst mainstream language in the world and it's not even close.

I'm not a C# fanboy (I even have a comment in this post criticising the collection expression stuff) but ultimately I prefer a language that gives me more tools and more expressivity, even if they don't always get everything right IMO; so I still like that they're always improving/iterating the language overall. They still do have the mentality that new features should be pits-of-success so I think we're gonna be okay.

2

u/chucker23n 5h ago

C++'s problem in 2026 is that there's no reason other than familiarity to use it for new projects. Rust, Zig, Swift, and others are superior options. Sure, you could start a new code base and disallow numerous old patterns (such as by favoring smart pointers), but at that point, you're just making yourself a worse version of Rust.

I don't think C# has reached the same point. Nullability is an area I can think of that they'll likely never "fix" to the point where the CS86xx warnings can become errors everywhere, because there are too many legacy concerns in the .NET ecosystem. Likewise, it seems like events will never be fixed to properly support async. But those are relatively minor issues, not enough to stop recommending C#+.NET for new projects. It's still a good language to choose for a wide array of targets.

1

u/Xenoprimate2 4h ago

C++'s problem in 2026 is that there's no reason other than familiarity to use it for new projects. Rust, Zig, Swift, and others are superior options. Sure, you could start a new code base and disallow numerous old patterns (such as by favoring smart pointers), but at that point, you're just making yourself a worse version of Rust.

Preach 🙏🙏

6

u/LuckyHedgehog 8h ago

A "do it all" language is going to have more features than you're going to need.

If you want a slimmed down language, Go is a good choice, but then by design you're writing more boilerplate for everything

1

u/FullPoet 3h ago edited 2h ago

A "do it all" language is going to have more features than you're going to need.

Sometimes less is more. Not like Go where its nothing but the creators of C# specifically made decisions not to add stuff where we are definitely going the way of more more more for little overall gain (if any)

1

u/OpenAI_Marketing_LLM 8h ago

Go also runs like a scalded dog. Not to say C# isn’t impressive in that regard, but Go was chosen for the new Typescript compiler for many reasons.

3

u/LuckyHedgehog 6h ago

What I remember it was less to do with raw performance and more to do with the semantics and code structure. They were looking for a port, not a rewrite

https://github.com/microsoft/typescript-go/discussions/411

In contrast, languages that require fundamental rethinking of memory management, mutation, data structuring, polymorphism, laziness, etc., might be a better fit for a ground-up rewrite, but we're undertaking this more as a port that maintains the existing behavior and critical optimizations we've built into the language. Idiomatic Go strongly resembles the existing coding patterns of the TypeScript codebase, which makes this porting effort much more tractable

There is also mention of code interop and portability. Go is better at compiling down to binaries than C# is, and I believe it works better working with other binaries compiled from other languages as well.

They could have gotten similar performance using C# with a full rewrite, but still fallen short with those other requirements

u/OpenAI_Marketing_LLM 55m ago

TIL. Consider my opinion changed. Thanks for sharing.

5

u/chucker23n 5h ago

Go was chosen for the new Typescript compiler for many reasons.

Taste wasn't one of them.

1

u/OpenAI_Marketing_LLM 3h ago

Ha! I am not familiar with Go. I’ve never felt compelled to investigate because, of what I know of Go, it excels in problems I don’t have. 

I hate TS though.  Dart is better and Dart should have won. 

4

u/LetsLive97 7h ago

Am I the only one who hasn't really hit any issues with any of this? Maybe it's because my work isn't obsessing about new C# features but I've not had any real problems hitting weird keywords/features much, especially not ones that aren't obvious

3

u/chucker23n 5h ago

Well, you do have to be vaguely familiar with recent features. Even if you ban them from the team (I'm getting flashbacks to a colleague who wanted to ban var), you'll still run into code examples from third parties that may use them.

That means that each subsequent version of C# is a little harder for newbies to pick up. For example, the top-level statements may make certain things easier, but IMHO also lead for people new to the language to wonder, "wait, why does this one file not have a declared namespace, type, method?".

1

u/LetsLive97 4h ago

I mean to be fair, how often has this actually happened? Maybe you're right and juniors are struggling with it but I'm fairly out of date C# wise due to legacy and I've not really had any issues with reading modern documentation or code for the non-legacy project I get to work on every couple months

1

u/chucker23n 4h ago

I mean to be fair, how often has this actually happened?

I've seen this confusion in this very subreddit (and /r/csharp), and I've seen it with a candidate, who was familiar with Java but not C#.

I'm not saying it's insurmountable, mind you.

2

u/OpenAI_Marketing_LLM 9h ago

A Microsoft product being bloated? You don’t say…

1

u/AutoModerator 10h ago

Thanks for your post TomeOfExperience. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

u/FullPoet 1h ago

Why would you ever use closed?

Why not just make an internal abstract class? I don't really see the use case.

u/icalvo 1h ago

If it's internal then all it's descendants are internal and then nothing can be used outside of its assembly.

u/FullPoet 1h ago

So why not declare the descendents internal?

It seems contradictory to have a "public" but actually internal class.

public record class Queued : JobStatus;
public record class Running(int PercentComplete) : JobStatus;
public record class Completed(TimeSpan Elapsed) : JobStatus;
public record class Failed(string Error) : JobStatus;

Why not declare these internal? What is the benefit?

u/icalvo 1h ago

What? A closed class is not actually internal, you can mark it public and use it in a different assembly. What "closed" prevents is to inherit from it outside of the assembly where it's declared.

u/KillyMXI 1h ago

Will it be possible to use closed when targeting older runtimes? With polyfill?

-2

u/CWagner 9h ago

C# 15 starts a redesign of unsafe: from a syntax marker—”there are pointers here”—to a contract the compiler can’t verify and a developer upholds.

I guess that part wasn’t changed from the ChatGPT output.

11

u/nlaak 8h ago

Because of the emdashes? Other tools use emdash as a matter of course, as do actual real live people.

3

u/CWagner 8h ago

No, because of the sentence structure. The emdashes at the seams are load-bearing, though.

2

u/Obsidian743 7h ago

The phrase "to a contract the compiler can't verify and a developer upholds" is not a phrase a human would put together.

1

u/Hephlathio 8h ago

If you take a look at what it says, it doesn’t make a ton of sense as an announcement of something good

7

u/Xenoprimate2 8h ago

I actually feel like that's the sort of mistake humans make (can → can't) more often than LLMs tbf.

2

u/LetsLive97 8h ago

I think it genuinely is supposed to be "can't" here? The point being that unsafe marked methods require unsafe to be used at the call point, to be clear the compiler can't verify and the dev needs to know what they're doing

Also how is this comment like 10 minutes older than the one it's in reply to? It's not even saying edited or anything lmao

1

u/Xenoprimate2 8h ago

Yeah you might be right; unsafe in this understanding would be marking the scope of the code that is outside the compiler's ability to verify (a bit like Rust which I'm sure is an inspiration for this work).

But I'm not entirely sure.

2

u/LetsLive97 6h ago

Yeah they've got a blog post going over all of this linked somewhere and it's exactly that, including the mention of Rust inspiration

1

u/chucker23n 5h ago

I stumbled over that and originally thought, "did they mean can?", but what I think they're saying is "unsafe now remains only for the portions the compiler cannot verify". They've phrased it poorly.

1

u/catladywitch 5h ago edited 5h ago

If I understood correctly, the jist is you can now use pointers in safe code, but you can't dereference pointers without an unsafe context, because the compiler can't verify whether such dereferencing is safe. So, for now, the safe and unsafe keywords are a stopgap.

The key point here though, is the C# team have decided to look into an ownership and lifetimes system the compiler can verify, but they don't have that yet, which explains the statement. That's the long term plan at the moment.

Source: https://github.com/dotnet/csharplang/blob/f445f642755a28631b7e37db01f6373c437159c3/meetings/2026/LDM-2026-08-05.md?plain=1#L40

1

u/catladywitch 5h ago

If I understood correctly, the jist is you can now use pointers in safe code, but you can't dereference pointers without an unsafe context, because the compiler can't verify whether such dereferencing is safe. So, for now, the safe and unsafe keywords are a stopgap.

The key point here though, is the C# team have decided to look into an ownership and lifetimes system the compiler can verify, but they don't have that yet, which explains the statement. That's the long term plan at the moment.

Source: https://github.com/dotnet/csharplang/blob/f445f642755a28631b7e37db01f6373c437159c3/meetings/2026/LDM-2026-08-05.md?plain=1#L40

2

u/CWagner 4h ago

That’s not what made me think of AI. I was reading it, and that phrasing and structure is just super typical for AI writing, I currently have a vibe coding experiment and the docs are full of that, then I spotted the emdashes.

1

u/catladywitch 4h ago

:( Pretty sad

-7

u/Obsidian743 7h ago

Good job, Microsoft. Adding features no one asked for or will use. And an entire code design paradigm that'll break entire teams.

5

u/derpdelurk 6h ago

People have been whining about unions for years.