-
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]: Let ref structs implement interfaces and substitute into type parameters (VS 17.11, .NET 9) #7608
Comments
@MadsTorgersen @333fred Could we get this on a triage list? It feels like we've spent more hours in LDM working around this problem than we would actually fixing it. |
Some comments not directly-related, but interesting to consider: What about The unconstraint can solve the gap for concrete methods, but how about delegate types? The delegate type parameters can't be constrained, should we specialize for them? |
Also, @jaredpar for lifetime issues. My thinking was that every parameter would implicitly carry a "heap-safe-to-escape" lifetime, but maybe there's something I'm not thinking of. Obviously people will request more lifetime stuff in the future to workaround that, but I figured that could be an additional set of language features that we do later. |
Can you clarify an example with It somehow feels to me that this is not a constraint on a type but rather a contract of a callee: a promise of no-escape-to-the-heap. A lifetime feature rather than a property of a type itself. |
Rather than void M<T>()
where T : IDisposable
allow T : ref struct {
}
Disagree. As a class C<T> where T : ~box {
T _field;
}
C<Span<int>> // not good That is also why I prefer
No. You need to prevent all behaviors that are disallowed by a |
Yeah I agree with that. And I think you're right that we might want to use different syntax. But I think the basic concept is still the same here. |
There are a couple of other items. Essentially any
Effectively for all intent and purposes the compiler would treat it like a Another other item that's come up here is how such an anti-constraint should be applied to existing APIs in the core libraries. Consider for example that every variation of One approach to this is to simply go through and manually update every such Another item is that allowing The implication of DIM on a interface I1
{
public object M() => this;
}
// Error: implicit box in the implementation of M
ref struct S : I1
{
} Think the likely conclusion here is that Need to work out the rules for co / contra variance |
On top of all of those details that @jaredpar brings up, I'd really like to be able to use this anti-constraint with |
That particular issue is a bit of a thorny problem because there are APIs in ref struct Span<T> : allow T : ref struct {
// Error 1: ref field to ref struct
ref T data;
int length;
// Error 2: Can't use a ref struct as an array element
pulbic Span(T[] array)
} The error 2 type of problems are very thorny. Essentially there is existing public API surface area that directly violates what the anti-constraint would provide. There isn't a general solution to this that I can envision. Think the way to move forward here would be that the compiler simply ignores them. Essentially The error 1 type of problems are more fundamental. There is a proposal out for allowing |
I think the notion of anti-constraints needs to be considered very thoroughly. It's almost a full on massive change all on its own. What do The proposed |
I don't think they're intended to be a general purpose feature. I think they're only to be used where they enable the language to support additional functionality, but they also don't make sense as a normal constraint. A |
Put up a full proposal for this feature https://github.com/dotnet/csharplang/blob/main/proposals/ref-struct-interfaces.md |
Another alternative that was mentioned in the old issue is placing the public void M<ref T>(T o) where T : IDisposable
=> o.Dispose(); |
My main issue there @timcassell is that reads to me like "this must be a ref struct" as opposed to "this is allowed to be a ref-struct". That said, we'll def consider syntactic possibilities here when designing this out. |
What about |
The syntax here is the least interesting part :) We'll likely consider a bunch and settle on one the group feels conveys the idea the best and feels the most c#-y :) |
For me that is much too easily confused with supporting the ability to have
I agree it's a bit strange to have both As @CyrusNajmabadi mentioned though the syntax is the least interesting part. It is also likely the part we will end up debating the most in LDM. For now I would encourage people to focus on the behaviors and ensure they satisfy the scenarios you want to get out of this feature. |
The funny thing that having read the proposal and thought about it, it kind of makes sense to me so the only remaining question that I have is the syntax :) It's just not "what looks nice", but potential future enhancements. I think we should all imagine what |
How will calling methods on |
@TahirAhmadov I thought the same. But I don't really like "allow" syntax. I'd rather think of this not as "allow" but as "restrict the lifetime of values of certain type T". And I would like to see some work in this direction. E.g. if I didn't want some IDisposable like native resource handle to escape I would use similar mechanism. This can lay groundwork for more general lifetime and ownership features. I think it's important to think of them when designing syntax/rules. |
While the idea to have |
Will this also allow us to use For example, T ParseValue<T>(string str) where T : IParseable<T> allow T : string
{
if (typeof(T) == typeof(string))
{
return (T)str;
}
return T.Parse(str);
} |
Not without said features being explicitly designed and implemented. |
So I just got news of this proposal. There's a major problem here: "adding instance default interface methods to existing interfaces becomes universally source binary breaking". We depend on the ability to add default interface methods all over the place. I'm reading this and going "really?" This breaks the primary motivating feature to have default interface methods. I did come to an idea of how to fix it but this is probably bonkers hard: when you encounter a missing default interface method on a ref struct; jit the mehod and bail if the jit would emit a box operation. |
Default interface methods have always had edge cases where they do not work. For example the diamond problem where the runtime cannot pick the best DIM member results in runtime exceptions. This is just another case where DIM will have an edge case.
I suspect that would not be very effective in practice. Most of the DIM members I've seen rely on calling other available members in the type. That implicitly uses |
"That implicitly uses this which forces a boxing operation on the value.": My testing is that doesn't box a struct. The jitter is smarter than that. |
The assertion of the runtime team is that it does and that it breaks |
How does that not break default implementations on normal mutable structs? (As in, the default implementation generates nonfunctional code because it mutates a copy)? |
It's almost like ref struct : IInterface wants to be a new kind of interface. Taking existing generics that have constraint : IInterface won't work then. And now I don't care about the feature existing. |
It wouldn't work in general, but I think it would work for this feature to allow calling |
Question: would something like below become possible? Span<char> span = ...;
string str = string.Create(10, span, (destSpan, sourceSpan)=> { ... });
// or
string str = string.Create(10, (x, y, span), (destSpan, state)=> { ... }); |
Assuming all of the delegates are changed to have the new anti constraint and there is no capture it should be possible. |
In theory I'm not seeing why we couldn't update The new API would be string Create<TState>(int length, TState state, Func<Span<char>, TState> func); and we would change delegate T2 Func<T1, T2>(T1 arg1)
where T1 : allows ref struct
where T2 : allows ref struct; |
Just curious, because I doubt anyone uses it anymore, but how would delegate |
@timcassell BeginInvoke throws PlatformNotSupportedException on newer .NET runtimes since .NET Core, and it sounds like the feature being discussed here would require runtime support and thus exclusively be for future .NET runtimes. |
I guess it was already possible by declaring a delegate with non-generic ref struct, so it's a non-issue for this feature. |
My expectation is that these core delegates are all updated to have |
(If this comment should be in a different place, let me know.) I see that the proposal currently says that the
That doesn't present a huge problem in C#, since existing generic constraints already aren't propagated automatically. Would F# need to suppress its default automatic generalization and generic constraint propagation for this specific (anti-)constraint? That would be a pretty major departure for F#. For example, take these two existing F# functions: let f (x : 'T when 'T : struct) = ignore x
let g x = f x The generic constraint from val f : x:'T -> unit when 'T : struct
val g : x:'T -> unit when 'T : struct Would F# need to suppress the propagation of this anti-constraint in particular? I.e., for some function val f : x:'T -> unit when 'T : allows ref struct
val g : x:'T -> unit // No constraint? ? What if there were also another, regular constraint on val f : x:'T -> unit when 'T :> ISomeInterface and 'T : allows ref struct
val g : x:'T -> unit when 'T :> ISomeInterface // This would be strange. While those may be partly F#-specific design decisions, I think it is worth noting that the design of this feature in the runtime and in C# may have additional implications for F#, especially if fundamental BCL types like |
Think that is a question for F# language designers. C# behavior here is essentially following how constraints are modeled in IL. There is nothing stopping F# from providing a different presentation here. |
Yes, but my comment was in part about how the choices that the current design makes available to F# here don't seem ideal:
But it's entirely possible that I'm missing something or haven't thought things through enough, and that consuming this from F# will be simpler than I'm making it out to be. |
Given anti-constraints are different from other constraints, it would feel very appropriate if F# would treat them differently than it treats constraints today. |
This is how C# treats constraints today though: they do not propagate by default. This decision is following our existing patterns. |
Not really. The anti-constraint is about cancelling out a constraint that already exists. The way it currently works is that .NET has an implicit The "allows ref struct" anti-constraint cancels out the implicit |
It is indeed an intersection from that perspective, just like adding While it is removing a constraint from the consumer's perspective, it is also adding a constraint by which the declaring construct must abide... |
...Upon further thought, though, I think I'm back to where I was before. (Sorry for messing up the threading; I was on my phone.) I think we were using the same metaphor to refer to different things, or rather referring to the same thing from different perspectives (API consumer versus API implementer). Given my previous post:
And your response:
This emerges from the fact that the set of operations that can be applied to a union is the intersection of the operations that can be applied to every case in the union. From my other post (again, sorry for messing up the thread):
But by definition in a generic construct the type parameter is not yet concrete, so, since you can't box a Seen that way, it actually makes sense that F# might not auto-propagate this constraint unless explicitly specified, since, unlike all existing constraints utterable in C# or F#, it is not being intersected with the implicit Given two functions like this (imaginary syntax; F# does happen to already use let f (x : 'T when 'T : struct or 'T : not struct or 'T : byref<struct>) = ignore x
let g x = f x It would make sense that val f : x:'T -> unit i.e., with the default, implicit constraint union val f : x:'T -> unit when 'T : struct or 'T : not struct since Just because Footnotes
|
|
That would need to be a separate proposal. The inability to implement |
Just add the My use case that I was hoping to start using this for: public static Promise All(params ReadOnlySpan<Promise> promises)
=> All(promises.GetEnumerator());
public static Promise All<TEnumerator>(TEnumerator promises) where TEnumerator : IEnumerator<Promise>, allows ref struct
{
using (promises)
{
...
while (promises.MoveNext())
{
var p = promises.Current;
...
}
return ...;
}
}
Allowing ref struct as generics but disallowing them to implement interfaces kind of neuters the feature. |
While I agree that having this held back behind the preview language feature is undesirable, it's still available to the people who really want to use it. For example, I rewrote my HTML generation library to make use of the features. |
Ref structs implementing interfaces
Related Issues
Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-26.md#ref-structs-in-generics
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-10.md#ref-structs-implementing-interfaces-and-in-generics
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-22.md#ref-structs-implementing-interfaces
The text was updated successfully, but these errors were encountered: