Repository navigation
[API Proposal]: params ReadOnlySpan<T> overloads for existing params T[] overloads #77873
Description
EDITED 1/31/2024 by @stephentoub
Adding params to existing spans:
namespace System
{
public static class MemoryExtensions
{
public static bool TryWrite(this Span<char> destination, IFormatProvider? provider, CompositeFormat format, out int charsWritten, **params** ReadOnlySpan<object?> args);
}
public class String
{
public static string Format(IFormatProvider? provider, CompositeFormat format, **params** ReadOnlySpan<object?> args);
}
}
namespace System.Collections.Generic
{
public static class CollectionExtensions
{
public static void AddRange<T>(this List<T> list, **params** ReadOnlySpan<T> source);
public static void InsertRange<T>(this List<T> list, int index, **params** ReadOnlySpan<T> source);
}
}
namespace System.Collections.Immutable
{
public static class ImmutableArray
{
public static ImmutableArray<T> Create<T>(**params** ReadOnlySpan<T> items);
}
public struct ImmutableArray<T>
{
public ImmutableArray<T> AddRange(**params** ReadOnlySpan<T> items);
public ImmutableArray<T> InsertRange(int index, **params** ReadOnlySpan<T> items);
public sealed class Builder
{
public void AddRange(**params** ReadOnlySpan<T> items);
public void AddRange<TDerived>(**params** ReadOnlySpan<TDerived> items) where TDerived : T;
}
}
public static class ImmutableHashSet
{
public static ImmutableHashSet<T> Create<T>(**params** ReadOnlySpan<T> items);
public static ImmutableHashSet<T> Create<T>(IEqualityComparer<T>? equalityComparer, **params** ReadOnlySpan<T> items);
}
public static class ImmutableList
{
public static ImmutableList<T> Create<T>(**params** ReadOnlySpan<T> items);
}
public static class ImmutableQueue
{
public static ImmutableQueue<T> Create<T>(**params** ReadOnlySpan<T> items);
}
public static class ImmutableSortedSet
{
public static ImmutableSortedSet<T> Create<T>(**params** ReadOnlySpan<T> items);
public static ImmutableSortedSet<T> Create<T>(IComparer<T>? comparer, **params** ReadOnlySpan<T> items)
}
public static class ImmutableStack
{
public static ImmutableStack<T> Create<T>(**params** ReadOnlySpan<T> items);
}
}
namespace System.Diagnostics.Metrics
{
public sealed class Counter<T> : Instrument<T> where T : struct
{
public void Add(T delta, **params** ReadOnlySpan<KeyValuePair<string, object?>> tags);
}
public sealed class Histogram<T> : Instrument<T> where T : struct
{
public void Record(T value, **params** ReadOnlySpan<KeyValuePair<string, object?>> tags);
}
public readonly struct Measurement<T> where T : struct
{
public Measurement(T value, **params** ReadOnlySpan<KeyValuePair<string, object?>> tags);
}
public struct TagList : IList<KeyValuePair<string, object?>>, IReadOnlyList<KeyValuePair<string, object?>>
{
public TagList(**params** ReadOnlySpan<KeyValuePair<string, object?>> tagList);
}
public sealed class UpDownCounter<T> : Instrument<T> where T : struct
{
public void Add(T delta, **params** ReadOnlySpan<KeyValuePair<string, object?>> tags);
}
}
namespace System.Text
{
public sealed class StringBuilder
{
public StringBuilder AppendFormat(IFormatProvider? provider, CompositeFormat format, **params** ReadOnlySpan<object?> args);
}
}Adding new params span-based overloads (all of these have an existing corresponding params T[]):
namespace System
{
public static class Console
{
public static void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
public static void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
}
public abstract class Delegate
{
public static Delegate? Combine(params ReadOnlySpan<Delegate?> delegates);
}
public class String
{
public static string Concat(params ReadOnlySpan<object?> args);
public static string Concat(params ReadOnlySpan<string?> values);
public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args);
public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args);
public static string Join(char separator, params ReadOnlySpan<string?> value);
public static string Join(string? separator, params ReadOnlySpan<string?> value);
public static string Join(char separator, params ReadOnlySpan<object?> values);
public static string Join(string? separator, params ReadOnlySpan<object?> values);
public string[] Split(params ReadOnlySpan<char> separator);
public unsafe string Trim(params ReadOnlySpan<char> trimChars);
public unsafe string TrimStart(params ReadOnlySpan<char> trimChars);
public unsafe string TrimEnd(params ReadOnlySpan<char> trimChars);
}
}
namespace System.CodeDom.Compiler
{
public class IndentedTextWriter : TextWriter
{
public override void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
public override void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
}
}
namespace System.IO
{
public static class Path
{
public static string Combine(params ReadOnlySpan<string> paths);
public static string Join(params ReadOnlySpan<string?> paths);
}
public class StreamWriter : TextWriter
{
public override void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
public override void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
}
public abstract class TextWriter
{
public virtual void Write([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
public virtual void WriteLine([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> arg);
}
}
namespace System.Text
{
public class StringBuilder
{
public StringBuilder AppendJoin(string? separator, params ReadOnlySpan<object?> values);
public StringBuilder AppendJoin(string? separator, params ReadOnlySpan<string?> values);
public StringBuilder AppendJoin(char separator, params ReadOnlySpan<object?> values);
public StringBuilder AppendJoin(char separator, params ReadOnlySpan<string?> values);
public StringBuilder AppendFormat([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args);
public StringBuilder AppendFormat(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args);
}
}
namespace System.Text.Json.Nodes
{
public sealed class JsonArray
{
public JsonArray(params ReadOnlySpan<JsonNode?> items);
public JsonArray(JsonNodeOptions options, params ReadOnlySpan<JsonNode?> items);
}
}
namespace System.Text.Json.Serialization.Metadata
{
public static IJsonTypeInfoResolver Combine(params ReadOnlySpan<IJsonTypeInfoResolver?> resolvers);
}
namespace System.Threading
{
public class CancellationTokenSource
{
public static CancellationTokenSource CreateLinkedTokenSource(params ReadOnlySpan<CancellationToken> tokens);
}
public class Task
{
public static void WaitAll(params ReadOnlySpan<Task> tasks);
public static Task WhenAll(params ReadOnlySpan<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(params ReadOnlySpan<Task<TResult>> tasks);
public static Task<Task> WhenAny(params ReadOnlySpan<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(params ReadOnlySpan<Task<TResult>> tasks);
}
}Background and motivation
C# 12 is tentatively on a path to add support for params ReadOnlySpan<T>, and for that to take precedence over params T[]. That means in APIs where we currently have a params T[]-based overload, there’s a benefit to us adding a params ReadOnlySpan<T> overload: it’ll be preferred by the compiler, and the compiler will aim to stackalloc the span rather than using an array. As a result, any existing call sites for the params-based overload will, upon recompilation, switch to the span-based overload and allocate less.
(It's likely that params in general will be de-emphasized once collection literals also comes to the scene, hopefully also in C# 12. As such, the benefits of adding params here would primarily be about making existing code more efficient. I've not included in this write-up other places we might want additional span-based overloads where there aren't already params T[]-based overloads; we can continue adding such APIs via separate, dedicated proposals.)
API Proposal
This is a rough strawman, to be refined as the language features are refined.
ImmutableHashSet:
public static ImmutableHashSet<T> Create<T>(params ReadOnlySpan<T> items);
public static ImmutableHashSet<T> Create<T>(IEqualityComparer<T>? comparer, params ReadOnlySpan<T> items) ;
ImmutableList :
public static ImmutableList<T> Create<T>(params ReadOnlySpan<T> items);
ImmutableQueue:
public static ImmutableQueue<T> Create<T>(params ReadOnlySpan<T> items);
ImmutableSortedSet:
public static ImmutableSortedSet<T> Create<T>(params ReadOnlySpan<T> items);
public static ImmutableSortedSet<T> Create<T>(IComparer<T>? comparer, params ReadOnlySpan<T> items) ;
ImmutableStack:
public static ImmutableStack<T> Create<T>(params ReadOnlySpan<T> items);
Console:
public static void Write(string format, params ReadOnlySpan<object?> arg);
public static void WriteLine(string format, params ReadOnlySpan<object?> arg);
Activator:
public static object? CreateInstance(Type type, params ReadOnlySpan<object?> args);
String:
public static string Concat(params ReadOnlySpan<object?> args);
public static string Concat(params ReadOnlySpan<string?> args);
public static string Format(string format, params ReadOnlySpan<object?> args);
public static string Format(IFormatProvider? provider, string format, params ReadOnlySpan<object?> args);
public static string Join(char separator, params ReadOnlySpan<object?> values);
public static string Join(char separator, params ReadOnlySpan<string?> values);
public static string Join(string? separator, params ReadOnlySpan<object?> values);
public static string Join(string? separator, params ReadOnlySpan<string?> values);
Path:
public static string Combine(params ReadOnlySpan<string> paths);
public static string Join(params ReadOnlySpan<string> paths);
TextWriter (and derived types with overrides):
public virtual void Write(string format, params ReadOnlySpan<object?> arg);
public virtual void WriteLine(string format, params ReadOnlySpan<object?> arg);
StringBuilder:
public StringBuilder AppendFormat(string format, params ReadOnlySpan<object?> args);
public StringBuilder AppendJoin(char separator, params ReadOnlySpan<object?> values);
public StringBuilder AppendJoin(char separator, params ReadOnlySpan<string?> values);
public StringBuilder AppendJoin(string? separator, params ReadOnlySpan<object?> values);
public StringBuilder AppendJoin(string? separator, params ReadOnlySpan<object?> values);
CancellationTokenSource:
public static CancellationTokenSource CreateLinkedTokenSource(params ReadOnlySpan<CancellationToken> cancellationToken);
Task:
public static void WaitAll(params ReadOnlySpan<Task> tasks);
public static int WaitAny(params ReadOnlySpan<Task> tasks);
public static Task WhenAll(params ReadOnlySpan<Task> tasks);
public static Task<TResult[]> WhenAll<TResult>(params ReadOnlySpan<Task<TResult>[] tasks);
public static Task<Task> WhenAny(params ReadOnlySpan<Task> tasks);
public static Task<Task<TResult>> WhenAny<TResult>(params ReadOnlySpan<Task<TResult>[] tasks);
JsonArray:
public JsonArray(JsonNodeOptions options, params ReadOnlySpan<JsonNode?> items);
public JsonArray(params ReadOnlySpan<JsonNode?> items);
LoggerExtensions:
All of the BeginScope, Log, LogCritical, LogDebug, LogError, LogInformation, LogTrace, and LogWarning overloads already there, just with `params ReadOnlySpan<object?> args` instead of `params object?[] args`.
Additionally, the following already have ReadOnlySpan<T>-based overloads where there's also a params T[]-based overload, so we'd just need to add params to the existing method:
ImmutableArray:
public static System.Collections.Immutable.ImmutableArray<T> Create<T>( ReadOnlySpan<T> items);
ImmutableArray<T>.Builder:
public void AddRange(ReadOnlySpan<T> items);
Counter<T> (metrics)
public void Add(T delta, ReadOnlySpan<KeyValuePair<string, object?> tags);
UpDownCounter<T> (metrics)
public void Add(T delta, ReadOnlySpan<KeyValuePair<string, object?> tags);
Histogram<T> (metrics)
public void Record(T value, ReadOnlySpan<KeyValuePair<string, object?> tags);
Measurement<T> (metrics)
public Measurement(T value, ReadOnlySpan<KeyValuePair<string, object?> tags);
API Usage
Existing usage.
Alternative Designs
No response
Risks
No response
Activity
stephentoub commentedon Nov 4, 2022
cc: @CyrusNajmabadi, @RikkiGibson, @cston
NN--- commentedon Nov 4, 2022
params IEnumerable proposal is more generic, it doesn’t require ReadOnlySpan.
DaZombieKiller commentedon Nov 4, 2022
@NN---
params IEnumerable<T>wouldn't allow for the parameter storage to be stack allocated, so it would still require allocations (plus it'd result in interface calls to access the values).params Span<T>is more about efficiency and allocation reduction, which can't be achieved to the same degree withparams IEnumerable<T>.stephentoub commentedon Nov 4, 2022
Right, there's no benefit to us adding a
params IEnumerable<T>-based overload where we currently have aparams T[]-based overload, other than being able to accept anIEnumerable<T>in general (but that doesn't require params, and many of these cases already have anIEnumerable<T>-based overload). It's also not clear whetherparams IEnumerable<T>will even be a thing; from my perspective, collection literals makeparamslargely obsolete, since instead of writingM(1, 2, 3), you'd be able to writeM([1, 2, 3]), and the latter syntax would be usable regardless of whether the target wasparamsor not. If that's the direction C# goes (and it's still very early days, so this is just me speculating),params IEnumerable<T>might not be a thing, and even if it is, it's not clear to me if or where we'd care to use it in the core libraries.stephentoub commentedon Jan 31, 2024
I just re-went through all of our APIs in dotnet/runtime that either have params arrays today or have ReadOnlySpan parameters today, and prototyped out adding/updating everything that seems relevant. I've updated the top post with a revised concrete proposal to review, as the params span work should be landing in the compiler shortly. I've left out APIs that I would have liked but that at present don't appear like they'd yield any benefits (e.g. params span overloads for all the various Log methods on LoggerExtensions would be nice, but at present we'd need to immediately ToArray it for implementation reasons, defeating the benefits).
1 remaining item
stephentoub commentedon Mar 19, 2024
Can you share an example of where you'd want to use those as params? You're thinking about someone doing:
?
terrajobst commentedon Mar 19, 2024
"I don't always write my files byte by byte, but when I do, I prefer params" 🤣
viceroypenguin commentedon Apr 10, 2024
@stephentoub few things:
paramsmodifier to the various functions? are there other tasks that need to be done for these?i'd be happy to help with the top section for now, and then come back and discuss doing parts of the bottom section.
stephentoub commentedon Apr 10, 2024
Thanks for offering to help. It's basically done, just some tests to be added. We were waiting to put up a PR for it until the compiler support landed in this repo, which it just did a few days ago.
viceroypenguin commentedon Apr 10, 2024
great, thanks! looking forward to seeing this in net9. :)