Allow ref struct to implement interfaces#7625
Conversation
Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com>
|
|
||
| 1. Instances of it cannot be boxed | ||
| 2. Instances participate in lifetime rules like a normal `ref struct` | ||
| 3. The type parameter cannot be used in `static` fields, elements of an array, etc ... |
There was a problem hiding this comment.
In short, ref struct cannot appear on the heap, and the rules are meant to prevent that.
| A type parameter bound by `allow T: ref struct` has all of the behaviors of a `ref struct` type: | ||
|
|
||
| 1. Instances of it cannot be boxed | ||
| 2. Instances participate in lifetime rules like a normal `ref struct` |
There was a problem hiding this comment.
This is perfectly fine, but I also think we could slip some of this support to a second release if necessary. If we don't allow scoped/unscoped/etc then I think the default ref-struct rules are very useful and the cases where you need different lifetimes are pretty narrow.
There was a problem hiding this comment.
Not sure what you want to slip here. Instances of the type parameter need to behave like ref struct from a lifetime perspective else it's easy to create memory safety issues. That support is fairly easy to add. Conceptually it's just applying lifetime rules to any type which could be a ref struct but really nothing else changes.
There was a problem hiding this comment.
Not sure what you want to slip here
Just the lifetime modifier syntax like scoped. Agreed it may not be worth it to skip.
|
|
||
| ### Runtime support | ||
| This feature requires several pieces of support from the runtime / libraries team: | ||
| - Preventing default interface methods from applying to `ref struct` |
There was a problem hiding this comment.
Yeah, this is an important one that will likely need runtime work.
| void M() | ||
| { | ||
| // Error: both of these box if I1 is implemented by a ref struct | ||
| I1 local1 = this; |
There was a problem hiding this comment.
The runtime will demand a this pointer as a boxed interface even without any of these explicit usages. Simply calling this method will break the runtime. So you're right that this needs a runtime exception.
|
|
||
| ```csharp | ||
| T Identity<T>(T p) | ||
| allow T : ref struct |
There was a problem hiding this comment.
*standard handwaves around syntax*
|
Thanks, @jaredpar. I read this with an eye towards dotnet/runtime#27229 (comment) and really wanting to get a solution there in for .NET 9. Nothing in this PR jumped out at me as being problematic for that, but wanted to double-check. |
|
@stephentoub Looked through the |
Co-authored-by: Joe4evr <jii.geugten@gmail.com> Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com>
agocke
left a comment
There was a problem hiding this comment.
LGTM, I think we can fill in details as needed
Co-authored-by: Charles Stoner <10732005+cston@users.noreply.github.com>
| } | ||
| } | ||
|
|
||
| ref struct S = I1 { } |
| ```csharp | ||
| interface I1 { } | ||
| I1 M1<T>(T p) | ||
| allow T : ref struct, I1 |
There was a problem hiding this comment.
It feels like the I1 constraint should only be permitted within a where clause not an allow clause
| ### Co and contra variance | ||
| To be maximally useful type parameters that are `allow T : ref struct` must be compatible with generic variance. Specifically it must be legal for a parameter to be both co/contravariant and also `allow T: ref struct`. Lacking that they would not be usable in many of the most popular `delegate` and `interface` types in .NET like `Func<T>`, `Action<T>`, `IEnumerable<T>`, etc ... | ||
|
|
||
| Given there is no actual variance when `struct` are involved these should be compatible. There is still some concern that I'm missing deeply generic variance cases. Need to sit down with @agocke to work out if this is truly safe or if there are deeply generic scenarios that need to be worked out. |
There was a problem hiding this comment.
I think it's good to double check. However, I doubt there are issues with nested generic conversion currently, because structs can only have invariant type parameters.
| Given there is no actual variance when `struct` are involved these should be compatible. There is still some concern that I'm missing deeply generic variance cases. Need to sit down with @agocke to work out if this is truly safe or if there are deeply generic scenarios that need to be worked out. | ||
|
|
||
| ### Auto-applying to delegate members | ||
| **Decision**: do not auto-apply |
There was a problem hiding this comment.
Agree that we should not auto-apply.
| While that is true it can present a problem in multi-targeted scenarios. Code would compile in one target framework but fail in another. This could lead to confusion with customers and result in a desire for a more explicit opt-in. | ||
|
|
||
| ### Binary breaking change | ||
| Adding `allow T: ref struct` to an existing API is not a source breaking change. It is purely expanding the set of allowed types for an API. Need to track down if this is a binary breaking change or not. Unclear if updating the attributes of a generic parameter constitute a binary breaking change. |
There was a problem hiding this comment.
A type parameter on an override inherits the base type parameter's constraints, so I would expect that adding allow T : ref struct would break something like the following: (SharpLab)
public interface I { }
public class C {
public virtual void M<T>(T t) where T : I // allow T : ref struct added in vNext
{
}
}
public class D : C {
public override void M<T>(T t)
{
I i = t; // used to be ok, but now is an error
}
}Depending on how the metadata for allow clause works, this case may or may not be a binary break, but it should probably be a source break as well as a runtime break.
This proposal sketches out what it would look like if
ref structwere allowed to implement interfaces and participate as generic parameters.Related #7608