Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
stephentoub opened this issue Nov 4, 2022 · 12 comments · Fixed by #100898 or #101499
Closed
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Memory
Milestone

Comments

@stephentoub
Copy link
Member

stephentoub commented Nov 4, 2022

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

@stephentoub stephentoub added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Memory labels Nov 4, 2022
@stephentoub stephentoub added this to the 8.0.0 milestone Nov 4, 2022
@ghost
Copy link

ghost commented Nov 4, 2022

Tagging subscribers to this area: @dotnet/area-system-memory
See info in area-owners.md if you want to be subscribed.

Issue Details

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

Author: stephentoub
Assignees: -
Labels:

api-suggestion, area-System.Memory

Milestone: 8.0.0

@stephentoub
Copy link
Member Author

cc: @CyrusNajmabadi, @RikkiGibson, @cston

@NN---
Copy link
Contributor

NN--- commented Nov 4, 2022

params IEnumerable proposal is more generic, it doesn’t require ReadOnlySpan.

@DaZombieKiller
Copy link
Contributor

@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 with params IEnumerable<T>.

@stephentoub
Copy link
Member Author

Right, there's no benefit to us adding a params IEnumerable<T>-based overload where we currently have a params T[]-based overload, other than being able to accept an IEnumerable<T> in general (but that doesn't require params, and many of these cases already have an IEnumerable<T>-based overload). It's also not clear whether params IEnumerable<T> will even be a thing; from my perspective, collection literals make params largely obsolete, since instead of writing M(1, 2, 3), you'd be able to write M([1, 2, 3]), and the latter syntax would be usable regardless of whether the target was params or 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 stephentoub modified the milestones: 8.0.0, 9.0.0 Jul 21, 2023
@stephentoub stephentoub self-assigned this Jan 31, 2024
@stephentoub stephentoub added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jan 31, 2024
@stephentoub
Copy link
Member Author

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).

@stephentoub stephentoub added the blocking Marks issues that we want to fast track in order to unblock other important work label Mar 13, 2024
@KrzysztofCwalina
Copy link
Member

It would be good to also add File span APIS: #99823

@stephentoub
Copy link
Member Author

It would be good to also add File span APIS: #99823

Can you share an example of where you'd want to use those as params? You're thinking about someone doing:

byte a = ..., b = ..., c = ...;
File.WriteAllBytes(path, a, b, c);

?

@terrajobst
Copy link
Member

"I don't always write my files byte by byte, but when I do, I prefer params" 🤣

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation labels Mar 19, 2024
@viceroypenguin
Copy link

@stephentoub few things:

  • are you looking for help with this?
  • for the first section: is it simply adding the params modifier 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
Copy link
Member Author

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
Copy link

great, thanks! looking forward to seeing this in net9. :)

@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Apr 11, 2024
@stephentoub stephentoub reopened this Apr 17, 2024
@stephentoub stephentoub removed the in-pr There is an active PR which will close this issue when it is merged label Apr 17, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Jun 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Memory
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants