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

[Proposal]: params in parameters #8301

Open
1 of 4 tasks
stephentoub opened this issue Jul 18, 2024 · 9 comments
Open
1 of 4 tasks

[Proposal]: params in parameters #8301

stephentoub opened this issue Jul 18, 2024 · 9 comments
Assignees
Milestone

Comments

@stephentoub
Copy link
Member

stephentoub commented Jul 18, 2024

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

@jaredpar
Copy link
Member

jaredpar commented Jul 19, 2024

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 when T is a ref 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.

@Mrxx99
Copy link
Contributor

Mrxx99 commented Jul 20, 2024

If params in is supported should params ref readonly also be supported? Or would this be implicitly the case?

@jjonescz
Copy link
Member

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.

@RikkiGibson
Copy link
Contributor

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'

@colejohnson66
Copy link

colejohnson66 commented Jul 24, 2024

Presumably, param scoped ref could allow the compiler to reuse the allocated span.

@jaredpar
Copy link
Member

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;

@RikkiGibson
Copy link
Contributor

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.

@colejohnson66
Copy link

colejohnson66 commented Jul 25, 2024

Sounds like we need lifetimes ala Rust ;)

Span['b]<char> M<'a, 'b>(scoped ref Span['a]<char> s)
    where 'b : 'a =>
    s;

@jaredpar
Copy link
Member

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

@333fred 333fred added this to the Working Set milestone Sep 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants