.NET Tools How-To's

Collection Expressions – Using C# 12 in Rider and ReSharper

ReSharper and Rider support for C# 12

Welcome to our series, where we take a closer look at the C# 12 language features and how ReSharper and Rider make it easy for you to adopt them in your codebase. If you haven’t yet, download the latest .NET 8 SDK and update your project files!

In this series, we are looking at:

This third part is all about collection expressions and how you can take full advantage of them with ReSharper and Rider!

Background and Syntax

In C#, we use many different types to represent collections, with various ways to create and initialize them. Many have verbose syntax, while some come with performance drawbacks, and together they make for a jumble of inconsistent code styles (ToList vs. ToArray, IEnumerable<T> vs. params T[], etc.). That’s plenty of motivation to sort it out and catch up with other languages like TypeScript or Python, which already successfully provide a succinct syntax. Some of the foundation for matching and deconstruction was already laid with the introduction of list patterns in C# 11. With C# 12, we can also enjoy the power of construction!

Let’s compare how collections were constructed before C# 12 and how much cleaner they can be written now:

// Pre C# 12
var array = new[] { 1, 2 };
var spread = array.Concat(new[] { 3, 4 });

// Post C# 12
int[] array = [1, 2];
int[] spread = [..array, 3, 4];

Since collection literals are target-typed, we cannot use var but must declare the type name for our variable. You can also see how the .. spread operator is used on the first array to integrate it into a new array with additional elements. Note that spreading could also be used between two elements.

IL Code and Conversion of Collection Expressions

You can use collection expressions to create collections for the most well-known collection types. The compiler automatically converts them to the appropriate target type:

IEnumerable integers1 = [1, 2];
IReadOnlyCollection integers2 = [1, 2];
IReadOnlyList integers3 = [1, 2];
ICollection integers4 = [1, 2];
IList integers5 = [1, 2];

Diving deeper into the low-level C# code generated by the compiler is always interesting. Feel free to take a look at our SharpLab snippet or to spin up the IL Viewer in ReSharper/Rider on your own solution to inspect some of these collection expressions:

IL Viewer on collection expressions

Note that when the target type is IReadOnlyList<T> or IEnumerable<T>, using collection expressions comes with an additional allocation cost. In the following example, the compiler would allocate an array object for storage but also wrap this array into a compiler-generated collection class which forbids modifications at runtime:

IReadOnlyList readOnlyList = [4, 8, 15, 16, 23, 42];

Collection expressions also solve the issue of conversion where elements of a collection are more derived than the target’s scalar type. While the compiler initializes the collection, it implicitly takes advantage of assignment compatibility and conversion operators:

object[] array1 = new[] { 1 };  // error: int[] cannot be converted to object[]
object[] array2 = [1];          // okay: inherits from object

AbsolutePath directory = Directory.GetCurrentDirectory();
string[] paths = [directory];   // okay: implicit conversion to string

Supporting Collection Expressions on Custom Types

Similar to the duck-typing approaches for awaiting, iterating, or deconstructing objects, you can support collection expressions for your own types! Assuming your type is a collection type, you just need to add the CollectionBuilderAttribute and provide a create method:

(File file1, File file2) = (null!, null!);
VirtualDirectory directory = [file1, file2];

public class File;

[CollectionBuilder(typeof(VirtualDirectory), nameof(VirtualDirectory.Create))]
public class VirtualDirectory : IEnumerable
{
    public static VirtualDirectory Create(ReadOnlySpan<File> items) => null!;
    public IEnumerator GetEnumerator() => null!;
}

Do you have an interesting use case? Please let us know in the comments!

Conversion and Simplification

As with many new language features, you may find yourself eager to quickly integrate them into your daily coding routines and establish new habits. You might also feel compelled to convert the entire codebase to use these shiny additions. Unsurprisingly, ReSharper and Rider have you covered!

You can use the Use collection expression quick-fix to convert arrays, lists, and other collection-like objects to the new, more succinct syntax:

Converting collection-like objects to collection expressions

The quick-fix also takes Add calls below the initializer into account:

Converting a list with consecutive Add calls

Of course, you can apply this quick-fix in bulk to save space and cognitive load throughout your solution, project, or file:

Converting to collection expressions in scopes

The bulk-fix may also bring you some peace of mind by unifying the various ways of passing empty collections:

Converting empty collections to collection expressions

Conversion Limitations

There are situations that ReSharper and Rider will handle more pessimistically to avoid possible runtime errors. At first, it might look like a good idea to convert the following method to use collection expressions:

IEnumerable<int> GetNumbers() => new List<int> { 1, 2, 3 };

However, we must anticipate that the caller may take advantage of the implementation and cast the results to a List<int>. In this case, converting to a collection expression would break the code with an InvalidCastException.

Code Style and Formatting

Every team has its unique taste regarding how code should be styled and formatted. ReSharper and Rider help you enforce your particular style through the Reformat Code action! As part of our collection expressions support, we’ve added new code style settings under some categories, like under the Line Breaks and Wrapping section:

Line Breaks and Wrapping settings for collection expressions

Or under the Tabs, Indents, Alignment section:

Tabs, Indents, Alignment settings for collection expressions

You can try these new settings by selecting a code block and invoking the Configure code style action. Alternatively, you can grab the following snippet and adapt the formatter comments:

// @formatter:initializer_braces end_of_line // end_of_line_no_space | next_line_shifted_2 | pico
// @formatter:space_within_list_pattern_brackets true
// @formatter:place_simple_list_pattern_on_single_line true
// @formatter:keep_existing_list_patterns_arrangement false
// @formatter:align_multiline_list_pattern true
// @formatter:wrap_list_pattern chop_if_long // chop_always | simple_wrap
// @formatter:max_array_initializer_elements_on_line 100001
int[] numbers = [ 1, 2, 3 ];

Future Work

We’re not done! We are still working on additional quick-fixes and context actions to make conversion easier for you. For example, we will support conversion for ToList, ToArray, and AsReadOnly invocations as well as simplifications to nested collection expressions.

Conclusion

In this post, we discovered several features related to the new C# 12 collection expressions. Try ReSharper 2024.1 EAP or Rider 2024.1 EAP to get the most out of the latest C# 12 in your daily work! If you see any opportunities for additional support, please let us know in the comments below!

image description

Discover more