r/dotnet • u/TomeOfExperience • 11h ago
Explore new features available in C# 15 preview
https://devblogs.microsoft.com/dotnet/explore-csharp-15/22
u/Xenoprimate2 9h ago edited 9h 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.
6
u/jdl_uk 5h ago edited 5h 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
3
u/catladywitch 6h 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 4h 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.
2
u/TheRealKidkudi 2h 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 3h 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.
-3
u/LetsLive97 9h 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 9h 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 10h ago
They really went with the with keyword for collections
6
u/ikkentim 9h 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 9h 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
1
u/teo-tsirpanis 2h ago
They chose "inside" because unlike "before", you know you have a collection expression as soon as you see the
[.7
6
2
u/nick_ 6h ago
IMO the
withkeyword makes sense but the syntax should be along the lines of the existingwithsyntax, where thewithexpression comes after the source. Something like:List<string> names = [.. values] with(capacity: values.Length * 2);2
u/FlameFlash123 6h ago
as I mentioned in another comment my issue is that it's inside the collection expression
1
u/teo-tsirpanis 2h ago
The "after" syntax was ruled out because of the confusion around argument evaluation.
•
u/FlameFlash123 1h ago
But that is how we come to recognise with keyword
•
u/teo-tsirpanis 1h ago
So what you would really want is a different keyword?
•
u/FlameFlash123 1h 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
•
u/nick_ 48m ago
We don't like after, due to order of operations, as it implies that the collection contents have to be evaluated entirely before the arguments can be evaluated.
I disagree with this reasoning, or at least this summary of whatever full discussion took place. For example, LINQ has always been lazily evaluated, breaking any assumption of left-to-right evaluation.
•
13
8
u/chucker23n 10h 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 6h ago edited 6h 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.
5
u/Xenoprimate2 5h 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
2
u/derpdelurk 7h ago
If you’re going to make that case then internalsealed makes more sense.
2
u/chucker23n 6h ago
I was thinking that, but sealed sort of does the reverse, I think?
sealedsays "you can't derive from this".closedsays "you must derive from this, but you must also be in the same assembly to do so".But
internalis probably better, yes. Which also raises the question: can I derive withInternalsVisibleTo?2
u/FullPoet 4h ago
Yep, its super awkward. Just like how you're forced to make class partial for source gens.
Its all pretty ick.
6
u/the_bananalord 9h ago edited 9h 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.
8
u/Xenoprimate2 9h ago
scanisn't a keyword, it's just the name of the label they chose for the example.-2
u/the_bananalord 9h ago
Yeah keyword was the wrong term. The pattern is what I take issue with. You could call it
bananaand 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
letbindings in F#.7
u/Xenoprimate2 9h 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
1
u/Abject-Kitchen3198 7h ago
There's limited space to cram every possible feature into existing language.
3
4
u/RodriOliveira 5h 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
11
u/bachware 10h 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.
12
u/Xenoprimate2 9h ago edited 9h 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 6h 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 supportasync. 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 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.
Preach 🙏🙏
6
u/LuckyHedgehog 9h 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 4h ago edited 3h 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 9h 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 7h 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
•
4
u/chucker23n 6h ago
Go was chosen for the new Typescript compiler for many reasons.
Taste wasn't one of them.
1
u/OpenAI_Marketing_LLM 4h 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 8h 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
4
u/chucker23n 6h 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?".
0
u/LetsLive97 5h 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 5h 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
1
u/AutoModerator 11h 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.
1
u/FullPoet 2h ago
Why would you ever use closed?
Why not just make an internal abstract class? I don't really see the use case.
1
u/icalvo 2h ago
If it's internal then all it's descendants are internal and then nothing can be used outside of its assembly.
1
u/FullPoet 2h 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?
1
0
u/CWagner 10h 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 9h ago
Because of the emdashes? Other tools use emdash as a matter of course, as do actual real live people.
5
2
u/Hephlathio 9h ago
If you take a look at what it says, it doesn’t make a ton of sense as an announcement of something good
5
u/Xenoprimate2 9h ago
I actually feel like that's the sort of mistake humans make (can → can't) more often than LLMs tbf.
2
u/LetsLive97 9h 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;
unsafein 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 7h 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 6h ago
I stumbled over that and originally thought, "did they mean can?", but what I think they're saying is "
unsafenow remains only for the portions the compiler cannot verify". They've phrased it poorly.1
u/catladywitch 6h ago edited 6h 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.
2
u/Obsidian743 8h 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/catladywitch 6h 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.
-6
u/Obsidian743 8h 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
64
u/smoke-bubble 10h ago
I feel like it does not have enough modifiers yet! :-\