-
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
generic constraint: where T : ref struct #1148
Comments
Would be nice also if // generic method is JITted independently for each combination of struct/ref struct type params
void Terminate<T>(T t) where T : ref struct, IAdvanceable
{
IAdvanceable a = t; // error: illegal conversion
while (t.Advance()) // statically dispatched
{ }
} There may be some syntax horror if interface disambiguation is required, because Again, this would be a somewhat limited capability, but I don't see why it can't run as far as |
Tagging @VSadov @OmarTawfik for thoughts. |
@lucasmeijer can you please explain more about how you envision this to tie in with #1147?
I guess you could have the compiler play detective on everything that originated from the Obviously, a new interface could be created, but that sorta defeats this:
I'm bringing this up because if we want to support the use case of having one method that accepts either Like, a method static void ActOnAllEvens<T>(listlike_parameter<T> lst, Action<T> action)
{
for (int i = 0; i < lst.Count; i += 2)
{
action(lst[i]);
}
}
static void Main()
{
Span<int> someList = stackalloc int[10];
/* populate someList */
ActOnAllEvens(someList, Console.WriteLine);
// lots of businessy code at my work returns ReadOnlyCollection<T>
ReadOnlyCollection<int> someCollection = SomeBusinessyCode();
ActOnAllEvens(someCollection, Console.WriteLine);
} That doesn't seem impossible to implement (minimally, you could make overloads for a well-defined set and then restrict how such a method could be overloaded, though that feels ugly), and IMO the code would look a lot cleaner than a magic set of generic type constraints. I don't expect anything like that to actually happen, but only because I don't see much value in the use case of being able to have one method that accepts either |
@airbreather I suspect you meant to ping me! You are absolutely right, I reckon that a
Yeah, that would certainly be nice. I think we can already wrap an |
@VisualMelon I should have, yes, though my first line was in reference to this line:
As for my reply...
My comments here were in response to:
I don't have data to support the following claim, but my intuition is that many "familiar APIs" have such "showstoppers". Each "showstopper" weakens the value of this part of the proposal, and I think that if it can't buy us the ability for
I was thinking that that would be the semantics of the generic type constraint proposed in this issue. My intuition is that nobody actually wants to literally constrain e.g., I see no fundamental reason why your
Can you please elaborate more on what you mean by this? As-written, that's true for some implementations of |
I don't this this would be a constraint on the type; as it wouldn't allow you to use a non-ref struct or class in the method; which I assume isn't the desire. Rather prevent boxing to the heap; so its more of a promise of what the method/class will do with the type; rather than constraining what the type can be. Perhaps to this end a modifier on the type would be more useful (like So public ref interface IReadOnlyIterable<T>
{
int Length { get; }
readonly ref T this[int index] { get; }
}
public ref interface IIterable<T> : IReadOnlyIterable<T>
{
new ref T this[int index] { get; }
}
public ref struct RefStruct<TIterable, T> where TIterable : IIterable<T>
{
TIterable iterable; // ok, is ref struct
void ForEach(Action<T> action)
{
var roiterable = (IReadOnlyIterable<T>)iterable; // ok, in ref inheritance chain
var obj = (object)iterable; // compile error, can't cast to non reflike
for (var i = 0; i < roiterable.Length; i++)
{
action(roiterable[i]); // ok
}
}
}
public struct NonRefStruct<TIterable, T> where TIterable : IReadOnlyIterable<T>
{
ref TIterable iterable; // error is not-ref struct
void ForEach(TIterable iterable, Action<T> action)
{
for (var i = 0; i < iterable.Length; i++)
{
action(iterable[i]); // ok
}
}
} |
Yes, on reflection I would be inclined to agree with your intuition. Even if the use of existing APIs is limited, I'd still be keen to see this capability, so that 'performance hungry' code isn't constrained in expressivity. Assuming it is technically feasible, my main concern is that the syntax could be misleading.
If that is the case... then all the better! However, there are good reasons why the
No, what I wrote doesn't make any sense... terribly embarrassing...
This is interesting. Can you provide a more substantial example (i.e. with concept implementation and consumption)? If I'm understanding correctly, it could allay any concerns about code which would be correct when handling a reference-type being illegal/wrong when handling what might be a |
@VisualMelon extended the sample; that work? |
I could really use this feature as well in combination with #1147 for eliminating code redundancy in e.g. And I am sure lots of other possibilities will open up if we get it. C++/STL like code for example. |
Do we really need new constraint for that though? By ref structs cannot implement interfaces anyway if i remember correctly (and object -derived methods are expected to throw anyway) so boxing seems to be non issue. I think relaxing rules for ref structs to allow to use them as type argument in methods and delegates ONLY, should suffice? The benefit would be that existing net core API would instantly benefit from this change after making internal only changes to languages while constraint route would require changes in languages internally AND on surface then discovering ALL apis that would benefit from said constraint and apply this constraint accordingly |
I don't think so. An arbitrary generic method could attempt to box that |
Bumping this issue again also with allowing ref struct to implement interfaces.
Could we instead broaden the generic constraint? and use for example Not storing it on the heap means that boxing should not be allowed (so IL The fact that we can't build abstraction around |
For allowing interface calls on interface IAdder
{
void Add(int x);
void PlusPlus() => Add(1);
}
ref struct Adder : IAdder
{
public int Value;
void IAdder.Add(int x)
{
Value += x;
}
}
class Program
{
static void Perform<T>(ref T val) where T : ref struct, IAdder
{
val.Add(1);
// 💣 Implicit boxing
val.PlusPlus();
}
static int Main()
{
Adder a = default;
Perform(ref a);
return a.Value;
}
} I think we would currently throw while JITting the Obvious way out would be to error out if the ref struct doesn't implement all interface methods, but that would mean after updating a NuGet package reference that defines the We would probably need to annotate the interface in a way that disallows default interface methods for the interface. |
Shouldn't we disallow explicit interface implementation for ref struct anyway? (as they are only available through interface casting which is not compatible with ref struct) |
They're callable without boxing through the |
I see. So it means that default interface method on structs will always end-up into an implicit boxing , until there is a plan for the JIT to reify the default interface method at some point... Ok, not sure I want default interface method anymore... 😅 |
Removing the boxing later would be observable, and a breaking change. |
Yep unfortunately. I would probably prefer default interface to be renamed to |
I'd very much like this constraint added. It should not enforce that what you pass in is a ref struct, but only that your usage of the type is compatible with ref structs. My goal is to have heterogeneous lookup for our keyed collections, like so: class Dictionary<TKey, TValue>
{
bool TryGetValue<TOtherKey>(TOtherKey key, IEqualityComparer<TKey, TOtherKey> comparer, out TValue value) where TOtherKey : ref struct;
}
var stringMap = new Dictionary<string,int>();
var key = new ReadOnlySpan<char>(...);
var spanStringComparer = ...;
if(!stringMap.TryGetValue(key, spanStringComparer, out int foo))
{
stringMap.Add(new string(key), 0);
} Given the focus we have now on in-situ parsing, where we try to reuse buffers rather than copying them, this will enable reducing allocations. |
It might make more sense to make this a modifier of the generic parameter itself, not a constraint: public ref struct Span<ref T>
{
}
// or
public ref struct Span<refable T>
{
} This alleviates the As an aside: there is still an argument for |
While we're at it.... could we perhaps allow pointer types to be passed when T is constrained to be dotnet/runtime#13627 suggests that the only issue with allowing pointer types in generic is issue with boxing (as the runtime currently does not allow boxing pointers). Since While it does not solve all the problems (pointers are allowed on heap/regular fields, whereas It does look like it'll still require changes in the runtime / IL spec since ECMA-335 disallows pointer types in type parameter, so it might be just better to take dotnet/runtime#13627's approach though. |
Aside: you can do that with the public unsafe void M<T>(T* value)
where T : unmanaged
{
} |
Not quite. I meant something like |
You can't, because pointers are not objects (unlike other primitives and structs), so they can't be boxed: |
A generic constraint tells me that it can only be that, because constraints cannot say "this or that" only "this and that", so it wouldn't work with regular structs or classes. Perhaps an attribute on the generic type could do the trick instead of a constraint to indicate to the compiler that it does not escape. public void DoStuff<[DoesNotEscape] T>(T arg) |
Thinking about this more, I agree that IEnumerable<T>
// Allows everything inside, but restricts the consumer
IEnumerable<out T>
// Disallows some things inside (using T as a parameter), but gives the consumer additional options (can contravariantly cast) Compared to IEnumerable<ref T>
// Disallows some things inside (which could cause an instance of T to escape), but gives the consumer additional options (passing Span<byte> etc.) Like This also means that it should be perfectly safe to add As far as I can tell, this would prevent any sort of duplication of interfaces or delegates, since adding public interface IEnumerable<out T> {}
public interface IEnumerator<out T> {}
// Changed to
public interface IEnumerable<ref out T> {}
public interface IEnumerator<ref out T> {} Note that this doesn't impair any prior user of The same for delegates; why not change this? public delegate TResult Func<in T, out TResult>(T arg);
// Changed to
public delegate TResult Func<ref in T, ref out TResult>(T arg); To sum it up:
In the future, I can imagine all of this working (but perhaps not all from the same point in the future):
|
@IllidanS4 I like the syntax, |
@timcassell Existing implementers of IEnumerable can escape the values, but the actual precise types like IEnumerable and delegates cannot, in their own code (I am speaking about what's written inside |
@IllidanS4 Oh I see, I retract my statement. |
@IllidanS4 remember that |
@333fred It would work just fine, you just couldn't implement it by returning the same thing like for Say you have a class that implements In the case of The only situation where this could pose an issue would be if |
@IllidanS4 interface IEnumerator<out T> {
T Current { get; }
}
class Enumerator : IEnumerator<Span<int>> {
public Span<int> Current => /**/;
}
void M<ref T>(IEnumerator<T> enumerator) {
T current = enumerator.Current;
// object current_ = enumerator.Current; <-- error
// IEnumerator<object> enumerator_ = enumerator; <-- error
}
M(new Enumerator()); |
@atynagano It would be nice to be able to use an interface with interface Interface1<T>
{
T Method(T arg); // this is fine
}
interface Interface2<T>
{
T Method(T arg) => arg; // this is fine
}
interface Interface3<T>
{
T Method(T arg) => (T)(object)arg; // this is not fine
} The compiler would have to be aware of the latter two cases, but adding a default implementation shouldn't prevent the interface from being usable, so it would likely have to disallow both default implementations from being used, since it can't inspect the code. But that is more limiting because now you can't use any default implementation with Also it doesn't seem much consistent to me if you don't want interface Interface4<T>
{
T Method(Class<T> arg);
} Here, based on what you imply, the No, it's better to give the promise explicitly, be consistent about it, and let the compiler warn you when it would be broken, like what already happens for |
I think for that case, the compiler could just not implicitly implement And as for the Like @IllidanS4 said, the compiler needs to know if the |
I see, I was not aware of that case. I just didn't feel like making changes to all existing interfaces, but I like the |
For reference, rust language attaches |
related: |
There is a championed issue with a proposal available now. It has taken into account the feedback here. Going to close this one so we can centralize the conversations on the championed issue / proposal. Thanks for getting the convo started here and everyone for the feedback |
Today it's not possible to use a ref struct as a type argument to a generic method or method of a generic type. We'd love to see it become possible to:
this feature would be valuable to us especially in combination with: #1147
Design Meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#ref-struct-improvements
The text was updated successfully, but these errors were encountered: