July 9th, 2024

C# 13: Explore the latest preview features

Kathleen Dollard
Principal Program Manager

C# 13 is starting to take shape with features that focus on flexibility, performance, and making your favorite features even better for everyday use. We build C# in the open and at this year’s Microsoft Build we gave you a peek into what was coming in C# 13. Today, we want to share the current status of what you can try today in C# 13 and provide updates on features planned for this release and beyond. Let’s take a look at these new features in more detail.

Try C# 13 today

Before we dive into each new features of C# 13 you may be wondering how do you try it out.

You can find the latest previews of C# 13 in latest .NET 9 preview (Preview 6 at the time of writing) and in the latest preview of Visual Studio 2022-17.11. To access preview features, set your language version to preview in your project file:

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
      <!--other settings-->
      <LangVersion>preview</LangVersion>
      <!--other settings-->
   </PropertyGroup>
</Project>

params collections

C# 13 extends params to work with any type that can be constructed via a collection expression. This adds flexibility whether you are writing a method or calling it.

When the params keyword appears before a parameter, calls to the method can provide a comma delimited list of zero or more values. The following works in all versions of C#:

public void WriteNames(params string[] names)
   => Console.WriteLine(String.Join(", ", names));

WriteNames("Mads", "Dustin", "Kathleen");
WriteNames(new string[] {"Mads", "Dustin", "Kathleen"});
// Both of these Would output: Mads, Dustin, Kathleen

Note that you can call the method with either a comma delimited list of values, or an object of the underlying type.

Starting in C# 13 params parameters can be of any of the types supported for collection expressions. For example:

public void WriteNames(params List<string> names)
   => Console.WriteLine(String.Join(", ", names));

Whenever you call a method that has a parameter that is an IEnumerable<T>, you can pass the results of a LINQ expression. If the IEnumerable<T> parameter has the params modifier, you can also pass a comma delimited list. You can use a comma delimited list when you have constants and a LINQ expression when you need it:

public void WriteNames(params IEnumerable<string> names)
   => Console.WriteLine(String.Join(", ", names));

var persons = new List<Person>
{
   new Person("Mads", "Torgersen"),
   new Person("Dustin", "Campbell"),
   new Person("Kathleen", "Dollard")
};

// All of the following output: Mads, Dustin, Kathleen
WriteNames("Mads", "Dustin", "Kathleen");
WriteNames(persons.Select(person => person.FirstName));
WriteNames(from p in persons select p.FirstName);

Overload resolution

When authoring a method, you can supply multiple params overloads. For example, adding an IEnumerable<T> overload supports LINQ and adding a ReadOnlySpan<T> or Span<T> overload reduces allocations, which can improve performance.

public void WriteNames(params string[] names)
   => Console.WriteLine(String.Join(", ", names));

public void WriteNames(params ReadOnlySpan<string> names)
   => Console.WriteLine(String.Join(", ", names));

public void WriteNames(params IEnumerable<string> names)
   => Console.WriteLine(String.Join(", ", names));

When one of the specified types is passed, that overload is used. When comma delimited values or no values, are passed, the best overload is selected. Using the overloads above:

// IEnumerable overload is used
WriteNames(persons.Select(person => person.FirstName)); 

// array overload is used
WriteNames(new string[] {"Mads", "Dustin", "Kathleen"}); 

// most efficient overload is used: currently ReadOnlySpan
WriteNames("Mads", "Dustin", "Kathleen");                

Multiple overloads can add convenience and improve performance. Library authors should give all overloads the same semantics so that callers don’t need to be concerned about which overload is used.

lock object

.NET 9 includes a new System.Threading.Lock type for mutual exclusion that can be more efficient than locking on an arbitrary System.Object instance. The System.Threading.Lock type proposal has more about this type and why it was created. Over time, this type is expected to become the primary mechanism used for most locking in C# code.

C# 13 makes it easy to use this type. When the compiler recognizes that the target of the lock statement is a System.Threading.Lock object, C# now generates calls to the System.Threading.Lock API and provides warnings for cases where an instance of a Lock might be incorrectly treated as a normal object.

This update means the familiar syntax for the lock statement leverages new features in the runtime. Familiar code gets better with minimal change. Just change your project’s TargetFramework to .NET 9 and change the type of the lock from object to System.Threading.Lock:

public class MyClass 
{
    private object myLock = new object();

    public void MyMethod() 
    {
        lock (myLock)
        {
           // Your code
        }          
    }
}

public class MyClass 
{
    // The following line is the only change
    private System.Threading.Lock myLock = new System.Threading.Lock();

    public void MyMethod() 
    {
        lock (myLock)
        {
            // Your code
        }     
    }
}

Index from the end in initializers

The index operator ^ allows you to indicate a position in a countable collection relative to the end of the collection. This now works in initializers:

class Program
{ 
    static void Main()
    {
        var x = new Numbers
        {
            Values = 
            {
                [1] = 111,
                [^1] = 999  // Works starting in C# 13
            }
            // x.Values[1] is 111
            // x.Values[9] is 999, since it is the last element
        };
    }
}

class Numbers
{
    public int[] Values { get; set; } = new int[10];
} 

Escape sequence \e

C# 13 introduces a new escape sequence for the character you know as ESCAPE or ESC. You previously had to type this as a variation of \u001b. This new sequence is especially convenient when interacting with terminals with the VT100/ANSI escape codes to System.Console. For example:

// Prior to C# 13
Console.WriteLine("\u001b[1mThis is a bold text\u001b[0m");

// With C# 13
Console.WriteLine("\e[1mThis is a bold text\e[0m");

This makes creating fancy terminal output easier and less prone to errors.

Partial properties

C# 13 adds partial properties. Like partial methods their primary purpose is to support source generators. Partial methods have been available for many releases with additional improvements in C# 9. Partial properties are much like their partial method counterparts.

For example, starting with .NET 7 (C# 12), the regular expression source generator creates efficient code for methods:

[GeneratedRegex("abc|def")]
private static partial Regex AbcOrDefMethod();

if (AbcOrDefMethod().IsMatch(text))
{
   // Take action with matching text
}

In .NET 9 (C# 13), the Regex source generator has been updated and if you prefer to use a property, you can also use:

[GeneratedRegex("abc|def")]
private static partial Regex AbcOrDefProperty { get; };

if (AbcOrDefProperty.IsMatch(text))
{
   // Take action with matching text
}

Partial properties will make it easier for source generator designers to create natural feeling APIs.

Method group natural type improvements

The natural type of an expression is the type determined by the compiler, such as when the type is assigned to var or Delegate. That’s straightforward when it’s a simple type. In C# 10 we added support for method groups. Method groups are used when you include the name of a method without parentheses as a delegate:

Todo GetTodo() => new(Id: 0, Name: "Name");
var f = GetTodo; // the type of f is Func<ToDo>

C# 13 refines the rules for determining the natural type to consider candidates by scope and to prune candidates that have no chance of succeeding. Updating these rules will mean less compiler errors when working with method groups.

allows ref struct

C# 13 adds a new way to specify capabilities for generic type parameters. By default, type parameters cannot be ref struct. C# 13 lets you specify that a type parameter can be a ref struct, and applies the appropriate rules. While other generic constraints limit the set of types that can be used as the type parameter, this new specification expands the allowed types. We think of this as an anti-constraint since it removes rather than adds a restriction. The syntax allows ref struct in the where clause, where allows indicates this expansion in usage:

T Identity<T>(T p)
    where T : allows ref struct
    => p;

// Okay
Span<int> local = Identity(new Span<int>(new int[10]));

A type parameter specified with allows ref struct has all of the behaviors and restrictions of a ref struct type.

ref and unsafe in async methods and iterators

Prior to C# 13, iterator methods (methods that use yield return) and async methods couldn’t declare local ref variables, nor could they have an unsafe context.

In C# 13, async methods can declare ref local variables, or local variables of a ref struct type. These variables can’t be preserved across an await boundary or a yield return boundary.

In the same fashion, C# 13 allows unsafe contexts in iterator methods. However, all yield return and await statements must be in safe contexts. These relaxed restrictions let you use ref local variables and ref struct types in more places.

Update on Extension Types

We are very excited about the Extension Types feature that Mads and Dustin showed at Build. We also described Extension Types in the blog post .NET announcements at Build. At the time, we were aiming for key parts of the feature to be in C# 13, but the design and implementation are going to take more time. Look for Extension Types in early C# 14 (NET 10) previews.

Summary

You can find more on these features at What’s new in C# 13. We’re still working on features for C# 13, and you can check out what we’re doing at the Roslyn Feature Status page. Also, be sure to follow the .NET 9 preview release notes where you can now find C# release notes for each release.

Download the latest preview of Visual Studio 2022-17.11 with .NET 9, check out these new features, and let us know what you think!

Author

Kathleen Dollard
Principal Program Manager

Kathleen Dollard loves to code and loves to teach and talk about code. She’s written tons of articles, a book, and spoken around the world. She’s on the .NET Team at Microsoft where she works on the .NET Core CLI and SDK and managed languages (C# and Visual Basic.NET). She’s always ready to help developers take the next step in exploring the wonderful world we call code.

51 comments

Discussion is closed. Login to edit/delete existing comments.

  • Ziv CaspiMicrosoft employee

    A new and more efficient lock type is nice, but it does nothing for those of us who have to maintain compatibility with N previous runtime versions (even .NET 4.7.2!) because other teams depend on us. How about the compiler automatically recognizes fields whose value is initialized once on construction and is never used outside of clauses? Then the compiler could automatically convert the type to the new lock type, immediately making...

    Read more
  • Philippe Paré

    Is it fair to assume that discriminated unions won’t make it to C# 13?

    • Kathleen DollardMicrosoft employee Author

      Correct. We continue to work on the right design for C#.

      • Philippe Paré

        Thank you for the info. Is there anything I can do to help with this? A long term goal of mine is to work on C# language design but I’m not exactly sure where to start apart from discussions on GH. Keep up the amazing work!

      • Kathleen DollardMicrosoft employee Author

        GH is a great place to start. CSharpLang is our design repo, Roslyn is has the implementation of C# and VB. Thus, design discussions are in CSharpLang, while things like Roslyn Feature Status are in Roslyn.

        We post our upcoming agenda and meeting notes. A peek at next week's agenda implies they'll be an update on unions from the working group in the meeting notes when they are posted. Notes are usually posted in about a...

        Read more
      • David Taylor

        I have survived fine without DU in C# and find it hard to tell if developer demand for it is just a fun meme or if developers are serious.

        Please only add it if you do some language innovation along with it. Maybe use a tuple like syntax, but with "OR (|)" for the possible returned types like this:

        <code>

        Then maybe something like a switch expression, but instead of "switch" use a word like "returning":

        <code>

        But I am...

        Read more
      • Kathleen DollardMicrosoft employee Author

        You actually included two features here: an anonymous union syntax (one of the things we are considering) and a variation of a switch statement - is not legal in a switch expression branch today.

        Do you think there should be a switch statement that works like a switch expression?

        In this code, why do you think the keyword is needed/helpful instead of .

        Read more
  • Heinrich Moser

    Partial Properties sound like something that could be used to reduce INotifyPropertyChanged boilerplate without the “annotate field” workaround currently used by third-party solutions – something WPF (and, I guess, UWP/WinUI) developers have been looking forward to for a long time.

    Are the plans from the WPF team to add a built-in source generator for that, now that partial properties are available?

    • Markus Hjärne · Edited

      There seems to be such plans in the CommunityToolkit.MVVM (aka MVVM Toolkit) (#555), which can be used with WPF, UWP, WinUI and others.

  • Markus Hjärne · Edited

    In the example of indexing from the end in initializers, it's a bit confusing that the Numbers.Values property has a setter. Since collection properties in .NET rarely have setters this kind of indicates that it is needed for the initialization to work. Wouldn't it work equally well if it didn't?

    <code>

    I was also surprised when I tried this code and it didn't compile in C# 12

    <code>

    Isn't that also an initializer and the left hand side an...

    Read more
    • Kathleen DollardMicrosoft employee Author

      That is a great question, and it points out a difference between collection initializers and object initializers that initialize a property that is a collection, such as the one in the post. Indexers do not work in collection initializers because they are generally not needed because you can just write:

      <code>

      We could look at that inconsistency, but it would introduce two ways to do the same thing which we work to avoid.

      It is interesting that this...

      Read more
      • Markus Hjärne · Edited

        But indexers DO in some cases work in collection initializers

        <code>

        which IIRC was added in a later version of C# than the original way of initializing dictionaries

        <code>

        So why should I think it wouldn't just work for arrays and List<T> too? And the description of the feature above only says "initializer", never "object initializer", which is why I tried it with a collection initializer in the first place.

        Although I agree that it makes more sense for dictionaries...

        Read more
      • Kathleen DollardMicrosoft employee Author

        making all initializers work as similar as possible lessens the cognitive burden

        I agree.

        I have a question, not pushing back, just wanting to understand the way you think about this.

        In the code below [1] is a key. Do you see the index position as roughly equivalent to a key when you are reading code because they both tell you where the value should go?

        <code>

        Read more
      • Markus Hjärne

        Thanks for clarification on the Numbers.Values setter.

        I don't really know how to read it, you tell me 😅 I didn't even know that you could initialize collections (except for dictionaries, where it's more intuitive) using that syntax before I read this blog post. That's why I tried it out, but instead of the more complicated example in the blog post (initializing an array property in an object initializer), I tried the simpler case (initializing an...

        Read more
      • Kathleen DollardMicrosoft employee Author

        You are correct. The setter on Numbers.Values is not necessary for the code to compile.

  • Dean Jackson

    Are comments on the Visual Studio blog intentionally turned off?

    • James MontemagnoMicrosoft employee · Edited

      Correct, they are turned off intentionally.

      • Heinrich Moser

        That much is obvious – the question was whether it was intentional. 🙂

        (And, of course, the implied question was: Why are they turned off? My personal guess is that the strong AI/cloud focus attracts negative comments, but, obviously, I don’t have proof for that.)

  • schbaem

    I hope C#14 once again has features that are of use to humans.

    • Kathleen DollardMicrosoft employee Author

      We’d love to hear what is most important to you. We are aware of the top issues in the repo, but it’s great when folks up vote issues that they care about.

      Although I’ll add, it was humans that asked for the work we’ve done so far in C# 13. I’m not sure we have any issues active submitted by non-humans 😉

      • David Taylor

        Be careful what you ask for Kathleen. GPT is getting quite smart.

  • Christian

    While I am disappointed that extensions won’t launch in C# 13 this does not come as a surprise because I regularly check the csharplang repo and the reasoning behind the decision is sound. I encourage other commenters who are wondering about the state of things to do the same. Hopefully extensions will be enabled in an earlier .NET 10 preview so developer feedback can be given early on.

  • Pedro Gil · Edited

    I feel upset the way you guys, lang designers, go to a conference and make people believe a so important feature like type extensions would ship in this next version, at the end, you realized you can't make it ship. Even for us, the blind ones who can't see the why it can't ship. filled our hopes and code aware of that feature and already expecting to be present.. and boom.. disappointment.
    I hope you...

    Read more
    • Kathleen DollardMicrosoft employee Author

      Thanks for letting us know how disappointing it is when we can't complete our plans.

      We work to be as open as practical - which includes sharing our best understanding of the plan when we speak about a feature Unfortunately, that means that if plans change it is disappointing. When plans change, we have internal discussions on how we could have known earlier, because it is not easy for us or our users when we need...

      Read more
  • Alfred White

    I'm not surprised by Extension Types falling out of the release, it is a very ambitious feature. But I am incredibly disappointed.

    To me it was the most exciting thing I've seen in C# since Span and without it there is nothing left in C# 13 (or 12...) that excites me, and nothing that I expect to use in the foreseeable future, aside from the field keyword if that makes it in. This is such a...

    Read more
    • Andrew Witte

      To me it feels like a some of C# is now to the point of running into the wall that is .NET.
      C# IMO has taking directions to solve problems that are work arounds to legacy IL choices, instead of solving them in a way thats best for C#. This is why a lang should never be designed around an IL that can't change to maintain linking to legacy binaries. Really .NET Core should have...

      Read more
      • Kathleen DollardMicrosoft employee Author

        It is true that C# relies on the .NET runtime, but I do not think it is a wall. For example, most of the performance improvements over each release are due to improvements in the runtime. We work closely with them to improve experiences - for example locking in .NET 9 where the runtime supplied the type and the hard work, and C# embraced it by recognizing it when you use the keyword and...

        Read more