-
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
[Proposal]: Delegate Type Arguments Improvements #5321
Comments
I wonder if we could simplify at all by building on ref struct ByRef<T>
{
ref T t;
public ByRef(ref T t) { ... }
} and the compiler could translate Then we would just need to make ref structs work through generics. |
So, I think the
|
One detail though is that the the generic approach will almost certainly throw C++/CLI for a loop. We'll need to deal with that. |
True. Odds are decent that the C++/CLI compiler will just crash when referencing an assembly that contains one of these types. I imagine we will also have to verify what happens when it sees ref fields. |
I'm convinced. The most valuable simplification would have been if we had built |
While we're looking into T GenericDivision<T>(Func<T dividend, T divisor, T /* quotient */> divide) Intellisense could then also suggest those names, both for lambdas and "generate method" refactorings. Aside from the uncertainty of whether it should be in here (or in its own Discussion/Proposal,) the one thing I'm 50/50 on is the naming of the return value for consistency (even though the encoded name could be used to suggest a variable name if the result of the delegate invocation is stored somewhere.) |
@BhaaLseN There is a brief discussion of how this could work in the bottom of the "Motivation" section of the proposal. Func<T, T, T> divide; // old way
T (T dividend, T divisor) divide; // new way The goal would be to bring the "expressiveness" of the types of lambda expressions up to par with the expressiveness of other kinds of function signatures as much as possible. It probably would not include the ability to name the return value since other places where we declare functions don't provide this. |
Oops, it looks like I read it but by the time i got to the end I had forgotten about it :D |
With all due respect, I completely disagree with the whole premise of this idea. On top of this, allowing these new syntax variants for generic types would need to work with all generic arguments, including regular classes and stuff. Otherwise, we have this dual feature - one style of generics for delegates and another for everything else. |
Somewhat related question, as a follow up to possibly supporting refs to refs if we do get support for ref types as generic arguments for delegate types: is this feature as a whole also tracking adding proper support for refs to refs in method parameters as a whole? If not, would it make sense to open a separate proposal for that? For instance, this would allow methods like this: bool TryGetRef(TKey key, out ref TValue value); This would also be possible through an |
This proposal was written with the intent to sidestep the concept of refs to refs. Even if we added the ability to pass ref arguments to type parameters in general we still might sidestep this concept. That said I would be interested to hear any arguments in favor of refs to refs and to hear more about your use case. |
For instance, consider these two APIs we've added to the public static class CollectionsMarshal
{
/// <summary>
/// Gets a reference to the value associated with the specified key, or returns Unsafe.NullRef<TValue>
/// </summary>
public static ref TValue TryGetValueRef<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key, out bool exists);
/// <summary>
/// Gets a reference to the value associated with the specified key, or inserts it with value default(TValue).
/// </summary>
public static ref TValue GetValueRefOrAddDefault<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key, out bool exists);
} They were unable to follow the proper public static bool TryGetValueRef<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key, out ref TValue value);
public static bool GetValueRefOrAddDefault<TKey, TValue>(Dictionary<TKey, TValue> dictionary, TKey key, out ref TValue value); Which would allow you to use them with the familiar and idiomatic try-something pattern: if (CollectionsMarshal.TryGetValueRef(map, out ref value))
{
// Do something with the value ref
} Now, this is just an example, but I can see something like this being generally useful as another option for people writing more lowlevel APIs. It's not unlike having methods taking (joking for a second, I'll also say that of course given that resources are limited, if the decision was made to extend generics in general and I could choose, I'd take being able to use pointers and ref structs in generics over this any day, because right now those limitations are an absolute chore to work around and generally much, much more impactful than this for devs 😄) |
Delegate Type Arguments Improvements
Summary
Allow a wider range of type arguments to delegates, such as the following:
Motivation
A long-standing guideline in .NET is to use System.Action and System.Func instead of custom delegates when possible. This makes it easier for users to tell at a glance what the delegate's signature is, as well as improving convertibility between delegates created by one component and handed out to another component.
However, there have always been "holes" in System.Action/Func. One of the big ones is that only by-value parameters and returns are supported. If you want to make a delegate from a method with a 'ref' or 'out' parameter, for example, you need to define a custom delegate type for it. The same issue exists for "restricted" types such as
ref struct
s, pointers, function pointers, TypedReference, etc.It would be easier to work with delegates generally if the standard Action/Func supported a wider range of parameter and return types. We could also make it so the compiler does not synthesize delegates for lambda implicit types in many more scenarios.
It would also help lay the groundwork for further features to make using delegate types easier, such as the ability to specify delegate parameter names at the point where the delegate type is used, a la tuple field names. See related discussion.
Detailed design
'ref' type arguments
'ref', 'in' and 'out' are permitted as "modifiers" to type arguments to delegates. For example,
Action<ref int>
. This is encoded usingSignatureTypeCode.ByReference
, and by using theInAttribute
andOutAttribute
as custom modifiers on the type arguments, like how such parameters are encoded on function pointers. This explicitly allows for overloading on 'ref' vs non-'ref' just as in conventional signatures.The compiler and .NET 7 runtime both need to be modified to support this. See the prototype section for more details.
The compiler will ensure that the type arguments in use are valid by checking the signature of the delegate after generic substitution. For example, we would give a compile error in the following scenario:
It's expected that some amount of composition will still be possible:
Other restricted type arguments
Pointers, function pointers, ref structs, and other restricted types would be generally permitted as type arguments to delegates, without the need to introduce any constraints to the delegate type parameters. The checks will occur in a similar fashion as for 'ref' type arguments.
Constraints
It seems like you could also "delay" constraint checks on usage of type parameters in delegate signatures and simply check the constraints when the delegate is used. It doesn't seem that useful, though.
It feels like this wouldn't actually present a usability benefit over requiring the declaration to specify the constraints. Let's not change the behavior here. If a delegate signature uses a type parameter, the constraints should still be checked at the declaration site of the delegate.
Why only delegates?
It's reasonable to wonder why we would only want to change the rules for delegates here, instead of allowing more interface-only types to participate, such as interfaces which don't use the type parameters in DIMs, or abstract classes which don't use the type parameters in non-abstract members. We might even wonder about whether this would be useful on some members which do have implementations.
Some of the issues with this include:
ref
type arguments to start getting errors, that's a problem. Also, the increased degree of indirection induced by a "check the construction" approach could make it more difficult to report meaningful errors on misuse. This means the feature would need to be driven by new type parameter constraints.ref
would probably need to be a separate constraint fromin
which would be separate fromout
. Pointers (where T : pointer
?) have different rules for their use than ref structs (where T : ref struct
) which have different rules for their use than restricted types.out
in particular is something that's really difficult to imagine generalizing in a useful way for members which have implementations, or even completely abstract types. (is there any interface you can imagine in .NET today which would have a sensible usage if it were given anout
type argument?)ref
start to crumble.The takeaway from all this is that it is still worthwhile to continue exploring a
where T : ref struct
constraint which could be used in any type kind, but the general "relaxation" of restricted type arguments described in this proposal probably only really makes sense for delegates.Prototype
The compiler and runtime teams were able to create a prototype which handles some simple scenarios with 'ref' and 'out' type arguments to 'System.Action'. See the compiler and runtime prototype branches.
Using a combination of compiler and runtime changes, a small end-to-end scenario like the following was found to work as expected (writing "01" to the console):
We were also pleasantly surprised by the fact that IL reading tools like ildasm and ILSpy handled the new encoding relatively gracefully:
We don't anticipate any significant roadblocks with allowing other restricted types as type arguments to delegates.
Thank you to @cston from the compiler team and @davidwrighton from the runtime team for their help in making the end-to-end prototype possible.
Drawbacks
Tools which read metadata will probably need to be updated to support this. The results from ildasm and ILSpy indicate that this might not be a great deal of work. We were able to get both tools to show
Action<ref int>
or equivalent, but ILSpy showedAction<out int>
asAction<ref int>
, for example.We also need to figure out how C++/CLI handle this: for example, will the C++/CLI compiler crash when loading an assembly that contains delegate types like this? If we move forward with this proposal we will need to give heads up so the tool can get updated in a timely manner.
Alternatives
'ref' constraint
It's possible this feature could be implementing by introducing a set of new "constraints" rather than by applying a "check the construction" approach. This is discussed in the Why only delegates section.
Do nothing
We don't gain the benefits outlined in the motivation section.
Unresolved questions
'in' vs 'ref readonly'
The language has a bit of a wrinkle where 'in' is used for readonly by-reference parameters, while 'ref readonly' is used for readonly by-reference returns. This raises a bit of a question on how a Func which both takes and returns readonly references should work, for example.
The question is essentially: which forms would be required when the type parameter is used for a parameter or return respectively? Do we care? Do we pick one form and disallow the other? Do we allow both interchangeably and silently use the "conventional" appearance in the symbol model, etc.
The type parameter could hypothetically be used for both parameter and return, so perhaps it is best to remain flexible and allow either
in
orref readonly
regardless of how the type parameter is used, or to pick one ofin
orref readonly
and disallow the other form in type arguments.Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-25.md#delegate-type-argument-improvements
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#delegate-type-arguments-improvements
The text was updated successfully, but these errors were encountered: