-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Champion "Permit IEnumerable<T> as the type of a params parameter" #179
Comments
I would assume that params was originally introduced so that the programmer didn't have the duality of having to write two methods just so that a piece of functionality could work with either a T or a collection of T, such as below:
While this was great, unfortunately as soon as IEnumerable was introduced and it became the de facto standard base collection type we're back to the duality again:
Actually, since we're on the subject, I'd like to propose that not only is
This works because at the callsite of method
the call:
gets converted to:
So the This is desirable because, as an API designer, it can sometimes feel dangerous to accept |
@Richiban I'm not sure I agree. There are many extension methods on Also, this introduces a lot of complexity in terms of the number of possible signatures and potentially unexpected type requirements and allocations. What if I want I think |
I have so many methods that take |
It would require teh compiler having some baked in knowledge, but i would also love "params ImmutableArray array" support. Perhaps you could have "params SomeType array" (where SomeType is a concrete type) and the compiler would look for "SomeType.Create(...)" as what it would call in that case. |
I don't see any issue with
|
@CyrusNajmabadi that certainly would be useful but if infrastructure is going to be put in place for using such patterns to instantiate There have been many requests for a collection literals as well as for JSON literals (collection literals) so if such a pattern is codified it would be great it were generalized to enable these scenarios as well. Personally I think collection literals would be great, but only if you could customize the target type to be, as you say, I don't think JSON literals make any sense at all but people ask for them. |
I think that talk of the compiler automatically inserting calls to extension methods is taking it a bit far... What is very easy to understand and (I'm assuming) easy to implement would be that anywhere you could previously write this: public void MyMethod<U>(M<U> items)
{
// Do stuff...
}
public void Callee()
{
MyMethod(new [] { 1, 2, 3 });
} You can now write this: public void MyMethod<U>(params M<U> items)
{
// Do stuff...
}
public void Callee()
{
MyMethod(1, 2, 3);
} and there's no further magic than that. |
I think accepting any type that is assignable from |
|
there is a special case that does not clearly translate to (EDIT: used two null args to trigger array creation) void M(params string[] args) {}
M(null, null); // OK args = new string[] { null, null }
void M(params IEnumerable args) {}
M(null, null); // ERROR? args = new[] { null, null } won't help |
That is not how it works. A method will be called in unexpanded (normal) form preferentially to being called in expanded form. |
Given the variety of solutions now proposed, and especially considering the latest #1366 issue, would it make sense for the compiler to support any type that supports collection initialization? This would mean we could put The callsite is where the magic happens. Anywhere it would be possible to type: It would also be possible to type: Or is that crazy? EDIT: Actually, it is crazy, and doesn't really help with all the interfaces. Ignore me! |
I like that too, though dotnet/runtime#22928 would give most of the benefits with Speaking of which, I'm in favor of I'd be perfectly satisfied if all I had were:
Given those, even if I found myself in a situation where I could really, really benefit from knowing the count in advance if available without forcing void Do(params IEnumerable<int> args)
{
int count;
switch (args)
{
case null:
throw new ArgumentNullException(nameof(args));
case ICollection<int> coll1:
count = coll1.Count;
break;
case IReadOnlyCollection<int> coll2:
count = coll2.Count;
break;
default:
List<int> argsList = args.ToList();
args = argsList;
count = argsList.Count;
break;
}
// count is now available to us
// same idea works for IList<T> / IReadOnlyList<T> if the indexer matters too
} |
I can't tell you how much I would love this feature. |
I'm working on a prototype for this (dotnet/roslyn#24080). here's my open questions for LDM to confirm:
|
Do you mean that it's trivial? Or that it's nontrivial? The 'not' in there is throwing me off. |
I think this would be fantastic. One of hte primary concerns around params in teh first place (and thus extending it to more situations) is that it incurs a array allocation. If we could have nice synchronous variadic APIs that didn't require allocations, i think that would be fantastic for the ecosystem. |
I meant the former; just not wanted to stress on "trivial". I've implemented that part by relaxing some checks in the compiler (and because an implicit conversion exists, there was no need to adjust lowering), but since it's not thoroughly tested I wasn't confident that's all it would take to support it. |
Perhaps instead of using
|
@TonyValenti: nice idea. Ideally this would work with immutable data structures, too. Maybe search for some conversion operator? |
@TonyValenti The downside with that is that if the user already has a variable of type |
This should have been done years ago. Please make it happen. |
It is disappointing that you can't pass a ReadOnlySpan/IEnumerable/IReadOnlyList variable as the item collection itself for a params parameter as it demands to be an array and therefore in most cases an unnecessary copy needs to be created. |
Personally, I like the idea of If not that, then |
It gets you Span and ReadOnlySpan, but with allocations. I’d rather params Span and params ReadOnlySpan were allocation-free... |
@MadsTorgersen Would be nice to get this in C#10, but I'm guessing the |
@AroglDarthu no the X in this case stands for ANY major version eg. It can equally likely happen in c# 20 or 11. (Btw c# 10 is practically done by now they ship GA in october if i remember correctly) |
@BreyerW Thanks 😊 It's labeled "Smallish Feature" (don't know if that means "small amount of work" or if it's considered "no huge benefit"). Perhaps it can be considered for the |
I would like to be able to use any collection type as the type of Params. Ie. params HashSet. |
There's a number of types that could be considered as part of this feature:
I wonder if this feature interacts with |
This would help make math libraries easier especially with INumber public static class Math
{
public static float Mean(params float [] a)
{
var result = 0f;
var count = 0;
foreach (var target in a)
{
result += target;
count++;
}
return result / count;
}
public static Vector2 Mean(params Vector2[] a)
{
var result = Vector2.Zero;
var count = 0;
foreach (var target in a)
{
result += target;
count++;
}
return result / count;
} to... public static class Math
{
public static T Mean<T>(params IEnumerable<T> a) where T : INumber
{
var result = default(T);
var count = 0;
foreach (var target in a)
{
result += target;
count++;
}
return result / count;
} |
I came up with a quick prototype for the holy grail "one params signature to cover all bases": Definitions
class ReadOnlyMemoryEnumerable<T> : IEnumerable<T>
{
public ReadOnlyMemoryEnumerable(ReadOnlyMemory<T> value) => this._value = value;
public IEnumerator<T> GetEnumerator() => new Enumerator(this._value);
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
class Enumerator : IEnumerator<T>
{
public Enumerator(ReadOnlyMemory<T> value) => this._value = value;
public T Current => this._value.Span[this._index];
object? IEnumerator.Current => this.Current;
public void Dispose() { }
public bool MoveNext() => ++this._index < this._value.Length;
public void Reset() => this._index = -1;
readonly ReadOnlyMemory<T> _value;
int _index = -1;
}
readonly ReadOnlyMemory<T> _value;
}
public ref struct Params<T>
{
public ref struct Enumerator
{
public Enumerator(Params<T> owner)
{
if (owner._enumerable != null)
{
this._enumerator = owner._enumerable.GetEnumerator();
this._spanEnumerator = default;
}
else
{
this._spanEnumerator = owner._span.GetEnumerator();
this._enumerator = null;
}
}
public T Current
{
get
{
if (this._enumerator != null) return this._enumerator.Current;
return this._spanEnumerator.Current;
}
}
public bool MoveNext()
{
if (this._enumerator != null) return this._enumerator.MoveNext();
return this._spanEnumerator.MoveNext();
}
readonly ReadOnlySpan<T>.Enumerator _spanEnumerator;
readonly IEnumerator<T>? _enumerator;
}
public static implicit operator Params<T>(T[] array) => new(new ReadOnlyMemory<T>(array));
public static implicit operator Params<T>(List<T> list) => new(list);
public Params(IEnumerable<T> enumerable)
{
this._enumerable = enumerable;
this._memory = default;
this._span = default;
}
public Params(ReadOnlyMemory<T> memory)
{
this._memory = memory;
this._span = memory.Span;
this._enumerable = null;
}
public Enumerator GetEnumerator() => new(this);
public IEnumerable<T> AsEnumerable() => this._enumerable ?? new ReadOnlyMemoryEnumerable<T>(this._memory);
readonly IEnumerable<T>? _enumerable;
readonly ReadOnlyMemory<T> _memory;
readonly ReadOnlySpan<T> _span;
}
public ref struct ParamsList<T>
{
public ref struct Enumerator
{
public Enumerator(ParamsList<T> owner)
{
if (owner._list != null)
{
this._enumerator = owner._list.GetEnumerator();
this._spanEnumerator = default;
}
else
{
this._spanEnumerator = owner._span.GetEnumerator();
this._enumerator = null;
}
}
public T Current
{
get
{
if (this._enumerator != null) return this._enumerator.Current;
return this._spanEnumerator.Current;
}
}
public bool MoveNext()
{
if (this._enumerator != null) return this._enumerator.MoveNext();
return this._spanEnumerator.MoveNext();
}
readonly ReadOnlySpan<T>.Enumerator _spanEnumerator;
readonly IEnumerator<T>? _enumerator;
}
public static implicit operator ParamsList<T>(T[] array) => new(new ReadOnlyMemory<T>(array));
public static implicit operator ParamsList<T>(List<T> list) => new(list);
public ParamsList(IList<T> list)
{
this._list = list;
this._memory = default;
this._span = default;
}
public ParamsList(ReadOnlyMemory<T> memory)
{
this._memory = memory;
this._span = memory.Span;
this._list = null;
}
public int Count => this._list?.Count ?? this._span.Length;
public T this[int index] => this._list != null ? this._list[index] : this._span[index];
public Enumerator GetEnumerator() => new(this);
public IEnumerable<T> AsEnumerable() => (IEnumerable<T>?)this._list ?? new ReadOnlyMemoryEnumerable<T>(this._memory);
public IList<T> ToList()
{
if (this._list != null) return this._list;
var res = new List<T>();
for (int i = 0; i < this._span.Length; ++i) res.Add(this._span[i]);
return res;
}
readonly IList<T>? _list;
readonly ReadOnlyMemory<T> _memory;
readonly ReadOnlySpan<T> _span;
} Usage: static void Foo(/*params*/ Params<int> list)
{
Console.WriteLine("Foo");
foreach (int x in list)
{
Console.WriteLine(x);
}
}
static void Bar(/*params*/ ParamsList<int> list)
{
Console.WriteLine("Bar: " + list.Count);
Console.WriteLine(list[0]);
Console.WriteLine(list[1]);
Console.WriteLine(list[2]);
}
int[] array = new[] { 1, 2, 3, };
var list = new List<int> { 4, 5, 6 };
IEnumerable<int> enumerable = new[] { 7, 8, 9 };
Foo(array);
Foo(list);
Foo(new Params<int>(enumerable));
//Foo(11, 12, 13);
Bar(array);
Bar(list);
//Bar(11, 12, 13); Basically, the method's author chooses what the method needs - enumeration only ( |
In light of collection expressions coming up, I wanted to revitalize this thread and suggest that ie. public static void Foo(params IEnumerable<int> x);
Foo(1, 2, 3); //same as Foo([1,2, 3]) This would play nice with target typing for collection expressions and solve the question of "What is the type of |
My understanding is that with collection literals the need for this proposal has kinda gone away as it's very light syntactically to just wrap the arguments in a collection literal rather than adding |
Correct. We don't see substantive value here in allowing this anymore, as you can always just do We do see substantive value in |
This proposal is now subsumed by the championed proposal #7700 for Params Collections. |
See also
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#params-ienumerable
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#params-improvements
The text was updated successfully, but these errors were encountered: