top of page

Explore the C# Params Collection Feature

In the dynamic programming world, languages are constantly growing by introducing new features and functionalities to improve developer productivity, code readability, and performance. One such language that consistently stays at the forefront of innovation is C#. In this, we will learn an exciting new feature in the C# Params Collection. This feature is currently in preview, and extends the params keyword functionality, traditionally limited to arrays, to a wider variety of collection types.

Explore the C# Params Collection Feature

Join us as we explore the ins and outs of this feature, its potential benefits, and how it could revolutionize handling collections in C#.


Let's get started!


C# Params Collection Feature

In earlier versions of C#, the params keyword was limited to array types. This means you could only use C# params with array parameters, allowing you to pass a number variable of arguments to the method.


Here’s an example of how the params keyword was used with array types in earlier versions of C#:

public static void PrintNumbers(params int[] numbers)
{
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

public static void Main(string[] args)
{
    PrintNumbers(1, 2, 3, 4, 5);
}

In this example, the PrintNumbers method accepts a variable number of integer arguments because of the C# params keyword. The arguments are passed as an array of integers (int[]). When calling PrintNumbers, you can pass any number of integers, treating it as an array inside the method. This was the limitation of the C# params keyword - it could only be used with array types. With the new enhancements in C#, this is no longer the case, and params can be used with other collection types.


However, with the enhancement, the C# params keyword is no longer limited to array types. You can now use params with any recognized collection type. This includes:

  • Support for System.Span<T> and System.ReadOnlySpan<T>, which are types that represent contiguous regions of arbitrary memory and are used for performance improvements.

  • Support for IEnumerable<T>: Any type that implements System.Collections.Generic.IEnumerable<T> and have an Add method. This means using params with types like List<T>, HashSet<T>, etc.

  • Support for interfaces like System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.IReadOnlyList<T>, System.Collections.Generic.ICollection<T>, and System.Collections.Generic.IList<T>. This allows for more flexibility when using the params keyword, as you’re no longer restricted to arrays and can use a wider variety of collection types.


This enhancement provides more flexibility when using the C# params keyword, as you’re no longer restricted to arrays and can use a wider variety of collection types. It allows for more efficient memory usage and can improve the performance of your code.


The Trade-off between Extra Array Allocation and Convenience

Imagine you’re developing a logging system for a high-performance application. This system has a method Log that accepts a variable number of arguments and writes them to a log file. Here’s how you might implement this method using the C# params keyword:

public void Log(params string[] messages)
{
    foreach (string message in messages)
    {
        // Write message to log file
    }
}

// Usage
Log("Starting application...", "Loading modules...", "Application started.");

In this example, every time you call the Log method, the runtime has to allocate an array to hold the messages. If your application generates a lot of log messages, this can lead to increased memory usage and potentially impact performance.


However, using C# params makes your code more concise and easier to read. You can pass the log messages directly to the Log method, without manually creating an array. This makes the code cleaner and more maintainable.


Now, let’s consider an alternative implementation where you manually create the array:

public void Log(string[] messages)
{
    foreach (string message in messages)
    {
        // Write message to log file
    }
}

// Usage
Log(new string[] {"Starting application...", "Loading modules...", "Application started."});

In this version, you will manually create the array before passing it to the Log method. This makes the code more verbose, giving you more control over the array allocation. You could, for example, reuse the same array for multiple Log calls, reducing memory usage.


So, there’s a trade-off when using the params keyword. While it makes your code more concise and easier to read, it can lead to increased memory usage due to the extra array allocation. Depending on the specific requirements of your application (e.g., performance, readability, maintainability), you might choose to use C# params or manually create the array. This decision should be made based on a thorough understanding of these trade-offs and the specific needs of your application.


Ability to add a C# params span overload

With the enhancements to the params keyword, you can now create a C# params overload that takes a Span<T> or ReadOnlySpan<T>. Unlike arrays, they don’t own the memory they point to, but provide a safe way to access and manipulate it. This means they don’t require any additional memory allocation, which can lead to significant performance improvements.


Params Span Overload: With the enhancements to the params keyword, you can now create a params overload that takes a Span<T> or ReadOnlySpan<T>. This allows you to pass a variable number of arguments to a method without the need for array allocation. Here’s an example:

public void Print(params ReadOnlySpan<string> messages)
{
    foreach (string message in messages)
    {
        Console.WriteLine(message);
    }
}

// Usage
Print("Hello", "World");

In this example, the Print method uses the params keyword with a ReadOnlySpan<string>. When you call Print("Hello", "World"), the runtime creates a span that points to the memory locations of the strings “Hello” and “World”. No additional array is allocated, which can lead to performance improvements, especially if the method is called frequently or with arguments.


Note: As of now params Span<T> or params ReadOnlySpan<T> are not yet supported directly in C#. The above example is a conceptual illustration of how it could be used if supported. The actual implementation might vary based on the final design decisions made by the language design team.


Changes to the Method Parameters

The changes to the Method parameters section are part of the proposed enhancements to the params keyword in C#. These changes are designed to extend the functionality of C# params to support a wider variety of collection types.


In the context of these changes, the Method parameters section in the C# language specification has been adjusted to accommodate the new parameter_collection concept. A parameter_collection consists of an optional set of attributes, a params modifier, an optional scoped modifier, a type, and an identifier.


Here’s how the formal parameter list looks after the changes:

formal_parameter_list :
    fixed_parameters
    | fixed_parameters ',' parameter_collection
    | parameter_collection
    ;

parameter_collection :
    attributes? 'params' 'scoped'? type identifier
    ;

This means that a parameter_collection declares a single parameter of the given type with the given name. This allows the params keyword to be used with different collection types, not just arrays.


How does the scoped modifier impact parameters with C# Params?

The scoped modifier in C# is used to restrict the lifetime of a value to the containing method. When applied to parameters or locals of type ref struct, local reference variables, including those declared with the ref modifier, or parameters marked with the in, ref, or out modifiers, the scoped modifier limits the lifetime of that value to be localized and reduced to the containing method.


This means the value cannot be passed to a called method, nor returned. This restriction ensures that the value does not escape the method scope, which can be important for maintaining the integrity of the data and preventing unintended side effects.


Here’s a simple example:

public void AppendFormatted(scoped ReadOnlySpan<char> value)
{
    // Omitted for brevity
}

In the above code, the AppendFormatted method accepts a ReadOnlySpan<char> parameter with the scoped modifier. The value parameter cannot be passed to another method or returned from the AppendFormatted method.


Parameter_collection concept

The parameter_collection is a new concept introduced in C# to extend the params keyword functionality. A parameter_collection consists of an optional set of attributes, a params modifier, an optional scoped modifier, a type, and an identifier. This means that a parameter_collection declares a single parameter of the given type with the given name.


How to Implement this feature?

The C# params collection feature is a proposed enhancement to the language and is not yet available in the stable release. However, once it’s released, you will be able to use it as follows:


let’s consider a scenario where you’re developing a method that needs to accept a variable number of strings. Here’s how you might implement this method using the C# params keyword with an array:

public void LogMessages(params string[] messages)
{
    foreach (string message in messages)
    {
        Console.WriteLine(message);
    }
}

// Usage
LogMessages("Info: Starting application...", "Warning: Low memory", "Error: Application crashed");

In this example, every time you call the LogMessages method, the runtime has to allocate an array to hold the messages. If your application generates a lot of log messages, this can lead to increased memory usage and potentially impact performance.


Now, let’s imagine that the proposed params collections feature is available. You could then use a List<string> instead of an array, which could be more efficient in terms of memory usage:

public void LogMessages(params List<string> messages)
{
    foreach (string message in messages)
    {
        Console.WriteLine(message);
    }
}

// Usage
LogMessages(new List<string> {"Info: Starting application...", "Warning: Low memory", "Error: Application crashed"});

In this version, you are passing a List<string> to the LogMessages method. Lists in .NET are implemented as dynamic arrays, meaning they can grow and shrink in size as needed. This can be more memory-efficient than using a fixed-size array, especially if the number of messages varies significantly.


Examples


Using C# params with List<T>:

public void Print(params List<int> numbers)
{
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

public static void Main(string[] args)
{
    Print(new List<int> {1, 2, 3, 4, 5});
}

In this example, the Print method accepts a List<int> as a parameter due to the params keyword.


Using C# params with HashSet<T>:

public void Print(params HashSet<int> numbers)
{
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

public static void Main(string[] args)
{
    Print(new HashSet<int> {1, 2, 3, 4, 5});
}

In this example, the Print method accepts a HashSet<int> as a parameter due to the params keyword.


Using C# params with Span<T>:

public void Print(params Span<int> numbers)
{
    foreach (var number in numbers)
    {
        Console.WriteLine(number);
    }
}

public static void Main(string[] args)
{
    Print(new Span<int>(new int[] {1, 2, 3, 4, 5}));
}

In this example, the Print method accepts a Span<int> as a parameter due to the params keyword.


Use of System.Span<T> and System.ReadOnlySpan<T>

Here’s an example of how you might use Span<T> to process a portion of an array without creating a new array:

int[] numbers = new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Span<int> slice = numbers.AsSpan().Slice(start: 5, length: 3);

foreach (var number in slice)
{
    Console.WriteLine(number);  // Outputs: 6, 7, 8
}

In this example, AsSpan().Slice(start: 5, length: 3) creates a span representing a portion of the original array. This operation does not allocate additional memory as the span is simply a “window” into the existing array.


This is particularly beneficial in high-performance scenarios where you need to process large amounts of data or want to avoid unnecessary memory allocations. For instance, when working with buffers, strings, or large data structures, using Span<T> and ReadOnlySpan<T> can lead to more efficient memory usage and faster execution times.


Conclusion

The new Params Collection feature in C# brings several benefits:

  1. Flexibility: It extends the params keyword to support different collection types, not just arrays. This means developers can use C# params with any recognized collection type, including System.Span<T>, System.ReadOnlySpan<T>, and types that implement System.Collections.Generic.IEnumerable<T> and have an Add method.

  2. Performance: The feature focuses on the use of the System.Span<T> and System.ReadOnlySpan<T> to reduce memory allocations. These types represent a contiguous region of arbitrary memory and are more efficient than arrays because they don’t require any additional memory allocation.

  3. Convenience: Using params makes your code more concise and easier to read, as you don’t have to create the array manually. Therefore, there’s a trade-off between the extra memory allocation (which can impact performance) and the convenience of using params.


Adopting this new feature could simplify existing code in several ways:

  1. Reduced Code Complexity: With the ability to use C# params with different collection types, developers can write cleaner and more concise code. They no longer need to manually create and manage arrays when they want to pass a variable number of arguments to a method.

  2. Improved Code Readability: Code that uses C# params with different collection types is easier to read and understand. This can make it easier for other developers to maintain and update the code.

  3. Enhanced Code Performance: By using C# params with System.Span<T> and System.ReadOnlySpan<T>, developers can write high-performance code that reduces memory allocations. This can be particularly beneficial in performance-critical applications.

bottom of page