Skip to content

[API Proposal]: params ReadOnlySpan<T> overloads for existing params T[] overloads #77873

@stephentoub

Description

@stephentoub

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions