Explore C# 13's enhanced params collections that extend beyond arrays to support Spans, Lists, and any collection type, enabling more flexible and performant method signatures.
One of the most significant improvements in C# 13 is the enhancement of the params modifier, which has been expanded far beyond its traditional array-only limitations. This feature represents a major step forward in making C# more flexible and performant for modern development scenarios.
Prior to C# 13, the params modifier was restricted to array types only. This limitation often forced developers into suboptimal patterns or required them to create multiple overloads to accommodate different collection types. C# 13 removes this restriction, allowing params to work with any recognized collection type, including Span<T>, ReadOnlySpan<T>, List<T>, and any type that implements IEnumerable<T> with an Add method.
The compiler can also work with collection interfaces like IEnumerable<T>, IReadOnlyCollection<T>, and IReadOnlyList<T>, synthesizing the appropriate storage when needed.
Span<T> and ReadOnlySpan<T> with params enables high-performance code without heap allocationsBefore this enhancement, developers were limited to arrays and often had to create multiple overloads or use less efficient patterns:
// Old approach: Limited to arrays only
public static void LogMessages(params string[] messages)
{
foreach (var message in messages)
{
Console.WriteLine($"[LOG] {message}");
}
}
// Required additional overloads for different types
public static void LogMessages(IEnumerable<string> messages)
{
foreach (var message in messages)
{
Console.WriteLine($"[LOG] {message}");
}
}
// High-performance scenarios required awkward workarounds
public static void ProcessData(ReadOnlySpan<int> data)
{
// Had to pass spans explicitly - no params support
for (int i = 0; i < data.Length; i++)
{
Console.WriteLine($"Processing: {data[i]}");
}
}
// Usage was inconsistent
LogMessages("Error", "Warning", "Info"); // Works with params
LogMessages(new[] { "Error", "Warning", "Info" }); // Array allocation
LogMessages(messageList); // Different overload
// High-performance calls were verbose
ProcessData(new ReadOnlySpan<int>(new[] { 1, 2, 3, 4, 5 }));
With C# 13, the same functionality becomes much more elegant and performant:
// Modern approach: Support for any collection type
public static void LogMessages(params IEnumerable<string> messages)
{
foreach (var message in messages)
{
Console.WriteLine($"[LOG] {message}");
}
}
// High-performance with Span support
public static void ProcessNumbers(params ReadOnlySpan<int> numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine($"Processing: {numbers[i] * 2}");
}
}
// List-based params for builder patterns
public static List<T> CreateList<T>(params List<T> items)
{
var result = new List<T>();
foreach (var item in items)
{
result.Add(item);
}
return result;
}
// Interface-based params with compiler synthesis
public static void ProcessItems<T>(params IReadOnlyList<T> items)
{
for (int i = 0; i < items.Count; i++)
{
Console.WriteLine($"Item {i}: {items[i]}");
}
}
// Usage is now consistent and performant
LogMessages("Error", "Warning", "Info"); // No array allocation
ProcessNumbers(1, 2, 3, 4, 5); // Stack-based, zero allocation
CreateList(existingList); // Direct list passing
ProcessItems("A", "B", "C"); // Compiler-synthesized storage
// Works seamlessly with collection expressions (C# 12)
var messages = ["Debug", "Info", "Warning"];
LogMessages(messages); // Direct collection passing
Here's a practical example showing the performance and usability improvements:
// C# 13: High-performance logging with zero allocations
public static class Logger
{
public static void LogWithContext(LogLevel level, params ReadOnlySpan<string> contextItems)
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
var context = string.Join(" | ", contextItems.ToArray());
Console.WriteLine($"[{timestamp}] [{level}] {context}");
}
}
// Usage: Clean, performant, and flexible
Logger.LogWithContext(LogLevel.Error, "UserService", "Authentication", "Token expired");
Logger.LogWithContext(LogLevel.Info, "OrderService", "Processing", $"Order {orderId}");
// Works with existing collections without conversion overhead
var contextParts = new[] { "PaymentService", "Validation", "Credit card" };
Logger.LogWithContext(LogLevel.Warning, contextParts);
Provide deeper integration with collection expressions to enable even more concise syntax:
// Suggested future syntax
ProcessItems([..existingItems, "new item"]);
Extend params support to IAsyncEnumerable<T> for async scenarios:
// Suggested future feature
public async Task ProcessAsync(params IAsyncEnumerable<string> items) { }
Allow generic constraints on params collections for better type safety:
// Suggested future syntax
public void Process<T>(params ICollection<T> items) where T : IComparable<T>
Provide built-in analyzers that suggest when to use Span-based params for performance-critical code.
Extend support for custom collection types with builder patterns similar to collection expressions.
The enhanced params collections in C# 13 represent a significant leap forward in the language's evolution toward more performant and flexible APIs. By removing the array-only restriction, Microsoft has enabled developers to write cleaner, more efficient code that aligns with modern .NET performance best practices.
This feature particularly shines in high-performance scenarios where memory allocations matter, while also improving the everyday developer experience by reducing the need for multiple method overloads. As libraries and frameworks adopt these patterns, we can expect to see more consistent and performant APIs across the .NET ecosystem.
The move toward collection-agnostic programming represents C#'s continued evolution as a language that balances developer productivity with performance, making it an excellent choice for both rapid application development and high-performance computing scenarios.