[Proposal]: `params in` parameters
stephentoub opened this issue ยท 9 comments
params in parameters
- Proposed
- Prototype: Not Started
- Implementation: Not Started
- Specification: Not Started
Summary
It should be possible for params
parameters to also be in
.
Motivation
I can have an in
parameter and initialize it with a collection expression:
using System.Collections;
using System.Collections.Generic;
C.M([1, 2, 3]);
public static class C
{
public static void M(in MyLargeStruct list) {}
}
public struct MyLargeStruct : IEnumerable<int>
{
public void Add(int i) {}
public IEnumerator<int> GetEnumerator() => null!;
IEnumerator IEnumerable.GetEnumerator() => null!;
}
And I can have a params
parameter and initialize it with a list of arguments (dropping the brackets of the collection expression):
using System.Collections;
using System.Collections.Generic;
-C.M([1, 2, 3]);
+C.M(1, 2, 3);
public static class C
{
- public static void M(in MyLargeStruct list) {}
+ public static void M(params MyLargeStruct list) {}
}
public struct MyLargeStruct : IEnumerable<int>
{
public void Add(int i) {}
public IEnumerator<int> GetEnumerator() => null!;
IEnumerator IEnumerable.GetEnumerator() => null!;
}
but it's currently an error to have both in
and params
:
public static void M(params in MyLargeStruct list) {}
error CS1611: The params parameter cannot be declared as in
I'm not aware of any reason why these shouldn't be allowed in conjunction. And with the introduction of collection expressions and their synergy with params, it's strange that one way of representing the same situation works and the other doesn't. For cases where a large struct is used and is thus desirable to be passed by reference, and where that struct is initializable with a collection expression, it'd be nice to be able to also allow someone to choose to use the params syntax.
The actual documentation for CS1611 refers to ref
and out
:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/params-arrays#method-declaration-rules
CS1611: The params parameter cannot be declared as in ref or out
and it makes sense that ref
and out
can't be used with params. But that same reasoning doesn't apply to in
. Maybe it just inherited the behavior and we never thought to fix it?
Detailed design
TBD
Drawbacks
TBD
Alternatives
TBD
Unresolved questions
TBD
Design meetings
I'm not aware of any reason why these shouldn't be allowed in conjunction.
I don't think there is anything fundamentally wrong but there are a few parts that we'd need to think through. Let's change up the example a bit to use a ref struct
collection.
C.M([1, 2, 3]);
public static class C
{
public static void M(in MyLargeRefStruct list) {}
}
[CollectionBuilder(typeof(MyLargeRefStruct), "Create")]
public ref struct MyLargeRefStruct
{
public IEnumerator<int> GetEnumerator() => throw null!;
public static MyLargeRefStruct Create(ReadOnlySpan<int> span) => throw null!;
}
Today the params collections feature has the following line:
Params parameters are implicitly
scoped
when their type is a ref struct. UnscopedRefAttribute can be used to override that.
The underlying motivation of this was to make params
naturally friendly to stackalloc
of the collections. Having implicitly scoped
values meant users couldn't escape the value so using stackalloc
at the call site was safe / unlikely to cause friction. When in
is inserted into the mix that friction angle goes way because the language doesn't support the notion of in scoped
yet. The best way can do here is scoped in
which still allows for the following:
// Okay
MyLargeRefStruct M1(params in MyLargeRefStruct s) => s;
// Error: can't escape value `s` to calling method
MyLargeRefStruct M2(params MyLargeRefStruct s) => s;
The behavior in M1
isn't wrong but it is the type of situation we specifically wanted to avoid when we designed params collections. If the language did support in scoped
I strongly suspect we'd end up designing params in
to be implicitly
in scoped
whenT
is aref struct
scoped in
otherwise
That gives me a little pause in doing in params
before ref scoped
. Maybe we could scope (hehe) it down to allowing the non ref struct
case.
If params in
is supported should params ref readonly
also be supported? Or would this be implicitly the case?
should
params ref readonly
also be supported?
That would not make much sense as one should not pass rvalues to ref readonly
parameters nor use them without ref
/in
callsite modifier.
The behavior in
M1
isn't wrong but it is the type of situation we specifically wanted to avoid when we designed params collections.
I think the main drawback is if the params in
parameter can be returned by value, then we can't reuse memory which is referenced by that value. e.g.
ReadOnlySpan<int> M(params in ReadOnlySpan<int> span) => span; // ok
// user code
var span1 = M([1, 2, 3]);
M([4, 5, 6]);
Console.Write(span1[0]); // needs to be '1'
Presumably, param scoped ref
could allow the compiler to reuse the allocated span.
Presumably, param scoped ref could allow the compiler to reuse the allocated span.
scoped ref
doesn't prevent this as the scoped
prevents the ref
from being returned but does nothing to prevent the value from being returned.
// Works
Span<char> M(scoped ref Span<char> s) => s;
To prevent the value, and the ref
, from being returned we'd need to support ref scoped
.
// Error
Span<char> M(ref scoped Span<char> s) => s;
Our ref lifetimes model is currently held back by the fact that every lifetime can be related to every other lifetime. For any two lifetimes, they are either the same or one is known to be bigger and the other smaller. But in order to do ref scoped, I think we want the ability to say that the referent's lifetime is not known to be either bigger or smaller than any other ref scoped referent. They are different lifetimes which have no known relation to any existing returnable or ref scoped lifetimes. (They would be defined as bigger than local scopes though so that locals can refer to them.)
That way you aren't struggling with, well, the ref is not returnable, unless you smuggle it out through a second ref scoped parameter.
Sounds like we need lifetimes ala Rust ;)
Span['b]<char> M<'a, 'b>(scoped ref Span['a]<char> s)
where 'b : 'a =>
s;
@colejohnson66 if you haven't read the proposal for ref scoped
yet you will probably find it interesting. The rough conclusion is that implementing ref scoped
is a fairly simple extension of the current model. It can also likely allow for ref
fields to ref struct
in a limited fashion.
At the same time it's also likely the limit of what we can achieve in C# without going to explicit lifetimes.