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

generic constraint: where T : ref struct #1148

Closed
lucasmeijer opened this issue Nov 25, 2017 · 51 comments
Closed

generic constraint: where T : ref struct #1148

lucasmeijer opened this issue Nov 25, 2017 · 51 comments

Comments

@lucasmeijer
Copy link

lucasmeijer commented Nov 25, 2017

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:

    class A<T> where T : ref struct
    {
        // T t;   <-- this would generate a compiler error. not allowed to leak a T to the heap.

         static void SomethingLegit(T t)
         {
                //totally fine to do ref-struct compatible things with t here.
         }
    } 

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

@VisualMelon
Copy link

Would be nice also if ref structs could implement interfaces, and these interfaces be generic constraints. Naturally the ref struct can't be converted to an object known to implement an interface, but the members could still be accessible on the type itself. If the runtime emitted independent functions for ref struct as it does for normal struct this ought to have no within-method cost. I don't know if the call-site is specialised or requires indirection, but this could be an overhead. I should expect that such generic specialisation would at least be applicable to performance-hungry domains that ref struct is targeting, even if an extra indirection is required (if I've correctly understood how it already works in the CLR).

// 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 ((IFace1)refTypeArg).CommonMethodName() at least looks like a conversion, but I don't know enough about the compiler to know if this is a significant technical concern.

Again, this would be a somewhat limited capability, but I don't see why it can't run as far as ref struct, and would allow, e.g. ReadonlySpan<T> to implement IReadonlyList<T>, which - while requiring a new method owing to the ref struct constraint requirement - would at least preserve implementation independence, and allow such existing and familiar APIs to be adopted from the outset.

@jcouv
Copy link
Member

jcouv commented Dec 1, 2017

Tagging @VSadov @OmarTawfik for thoughts.

@airbreather
Copy link

airbreather commented Dec 5, 2017

@lucasmeijer can you please explain more about how you envision this to tie in with #1147?

would allow, e.g. ReadonlySpan<T> to implement IReadonlyList<T>

IReadOnlyList<T> inherits from IEnumerable<T>, which has IEnumerator<T> GetEnumerator(). Without further changes, that enumerator could legally be stored off somewhere for MoveNext() to be called when the original span has gone out-of-scope.

I guess you could have the compiler play detective on everything that originated from the T parameter's methods, but that sounds to me like a ton of work (both for the developers to implement and for the compiler to validate) for an unclear amount of benefit.

Obviously, a new interface could be created, but that sorta defeats this:

allow such existing and familiar APIs to be adopted from the outset.

I'm bringing this up because if we want to support the use case of having one method that accepts either IReadOnlyList<T> or ReadOnlySpan<T> and then (after JIT) performs no worse than a method that accepted just one or the other, then let's make start an open-ended proposal for that, and then maybe the development and design effort would be better spent on a more specific language feature instead of serving as auxiliary justification for more weird generic type constraint soup to enable more ugly code.

Like, a method where TList : maybeRefStructButMaybeNot, IReadOnyList_RefStructCompatible<T> leaves a bad taste. But we're looking at designing / developing the language itself here, so we're not restricted to just thinking inside the box. We're writing the rules, so this is fair game (please go easy on this idea, I don't claim to be a good language designer, it's just a top-of-my-head idea to illustrate a point):

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 IReadOnlyList<T> or ReadOnlySpan<T>.

@VisualMelon
Copy link

VisualMelon commented Dec 5, 2017

@airbreather I suspect you meant to ping me!

You are absolutely right, GetEnumerator() would effectively preclude a ref struct inheriting from IEnumerable or IReadOnlyList etc. which wouldn't work. I'd completely missed that. However, I don't think this is a general concern: if an interface doesn't make sense to be implemented by a ref struct then that can be left to the ref struct's author's discretion, and the compiler will already prevent, for example, a 'correct looking' implementation of GetEnumerator(). I wouldn't support any 'detective' work on the compiler.

I reckon that a maybeRefStructButMaybeNot - horrendous as it looks - would be an interesting feature (has all the restrictions of a ref struct, but allows another things (perhaps just structs, I haven't thought the possibilities through). I should really further my understanding of ref structs: perhaps the ref struct constraint itself would allow ref variants of normal structs, or something daft like that. This became wishy-washy rather quickly...

I'm bringing this up because if we want to support the use case of having one method that accepts either IReadOnlyList or ReadOnlySpan

Yeah, that would certainly be nice. I think we can already wrap an IReadOnlyList<T> in a ReadOnlySpan<T>, so it shouldn't be a dire concern for new APIs.

@airbreather
Copy link

@airbreather I suspect you meant to ping me!

@VisualMelon I should have, yes, though my first line was in reference to this line:

this feature would be valuable to us especially in combination with: #1147

As for my reply...

However, I don't think this is a general concern: if an interface doesn't make sense to be implemented by a ref struct then that can be left to the ref struct's author's discretion, and the compiler will already prevent, for example, a 'correct looking' implementation of GetEnumerator().

My comments here were in response to:

I don't see why it can't run as far as ref struct, and would allow, e.g. ReadonlySpan<T> to implement IReadonlyList<T>, which - while requiring a new method owing to the ref struct constraint requirement - would at least preserve implementation independence, and allow such existing and familiar APIs to be adopted from the outset.

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 ReadOnlySpan<T> to stand in for pretty much any interface that ultimately inherits from IEnumerable<T>, then I'm really not seeing much value (which is why I prodded about #1147).

I reckon that a maybeRefStructButMaybeNot - horrendous as it looks - would be an interesting feature (has all the restrictions of a ref struct, but allows another things [...]

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 T to be a ref struct type; rather, the desire is to be able to use ref struct types as generic type parameters.

e.g., I see no fundamental reason why your Terminate<T> method should reject a reference-type argument that implements IAdvanceable.

I think we can already wrap an IReadOnlyList<T> in a ReadOnlySpan<T>, so it shouldn't be a dire concern for new APIs.

Can you please elaborate more on what you mean by this? As-written, that's true for some implementations of IReadOnlyList<T>, but there exist perfectly valid IReadOnlyList<T> implementations that cannot be wrapped in ReadOnlySpan<T>, such as implementations that compute their output on-the-fly.

@benaadams
Copy link
Member

benaadams commented Dec 5, 2017

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 out and in for variance).

So ref interface? Meaning the interface cannot be boxed; but you can still apply it to classes and non-ref structs?

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
        }
    }
}

@VisualMelon
Copy link

VisualMelon commented Dec 5, 2017

@airbreather

I don't have data to support the following claim, but my intuition is that many "familiar APIs" have such "showstoppers".

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.

I see no fundamental reason why your Terminate method should reject a reference-type argument that implements IAdvanceable.

If that is the case... then all the better! However, there are good reasons why the class and struct constraints exist, so I'm not sure that this is necessarily viable (again, I've not thought this through properly, but it feels too good to be true).

Can you please elaborate more on what you mean by this?

No, what I wrote doesn't make any sense... terribly embarrassing...

@benaadams

So ref interface? Meaning the interface cannot be boxed; but you can still apply it to classes and non-ref structs?

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 struct or ref struct if clearly annotated where the type is consumed.

@benaadams
Copy link
Member

@VisualMelon extended the sample; that work?

@nietras
Copy link

nietras commented Jan 8, 2018

I could really use this feature as well in combination with #1147 for eliminating code redundancy in e.g. Span.Sort (https://github.com/dotnet/corefx/issues/15329) that I am working on. Using this I could define a generic type argument for the case when just sorting a single span or when there is an extra Span<TValue> of items in the same code with zero overhead. And this is hundreds of lines of code. I can give an example of what I would do if people are interested.

And I am sure lots of other possibilities will open up if we get it. C++/STL like code for example.

@BreyerW
Copy link

BreyerW commented Aug 30, 2018

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

@HaloFour
Copy link
Contributor

@BreyerW

I don't think so. An arbitrary generic method could attempt to box that ref struct by casting to an object, or assigning to a field on a reference type somewhere.

@xoofx
Copy link
Member

xoofx commented Dec 15, 2018

Bumping this issue again also with allowing ref struct to implement interfaces.

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.

Could we instead broaden the generic constraint? and use for example where ref, which would say that it will not allow storing the passed variable to the heap. It would be a constraint on its usage, not on the type itself. So this would work with any type, ref struct, struct or even class.

Not storing it on the heap means that boxing should not be allowed (so IL box is forbidden on such variable), meaning that interface boxing would never be possible (although that's unfortunate that duck typing for IEnumerable will not work out of the box...), meaning that we could allow ref struct to implement interface with such constraint.

The fact that we can't build abstraction around ref struct is restricting a lot their (generic) usage. We can't develop algorithm around them, that's really annoying.

@MichalStrehovsky
Copy link
Member

MichalStrehovsky commented Dec 18, 2018

For allowing interface calls on ref struct-constrained generic arguments we need to consider the interplay with the default interface methods feature. Consider:

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 Perform method with an InvalidProgramException because JIT would attempt to generate boxing code for the ref-like type.

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 IAdder interface (and adds the PlusPlus method), we would get a build error, completely defeating the purpose of the default interface methods feature (the purpose being allowing library authors to add methods to their interfaces without breaking consumer code).

We would probably need to annotate the interface in a way that disallows default interface methods for the interface.

@xoofx
Copy link
Member

xoofx commented Dec 18, 2018

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)

@MichalStrehovsky
Copy link
Member

MichalStrehovsky commented Dec 18, 2018

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 constrained. (callvirt) IL instruction prefix, which is what the C# compiler emits in the Perform method above. For the purposes of interface dispatch within the Perform method above it doesn't matter whether the interface method is implemented implicitly or explicitly. What matters is where the resolution ends up going - whether to an instance method on a value type (where the this pointer is a byref), or whether to an instance method on an interface (where the this points to an object).

@xoofx
Copy link
Member

xoofx commented Dec 18, 2018

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

@MichalStrehovsky
Copy link
Member

until there is a plan for the JIT to reify the default interface method at some point

Removing the boxing later would be observable, and a breaking change.

@xoofx
Copy link
Member

xoofx commented Jan 10, 2019

Removing the boxing later would be observable, and a breaking change.

Yep unfortunately.

I would probably prefer default interface to be renamed to trait and keep normal interface as interface. We would then disallow a ref struct if it implements a trait instead of an interface.

@scalablecory
Copy link

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.

@jcdickinson
Copy link

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 maybeRefStructButMaybeNot problem that @airbreather mentioned: generic parameter modifiers (covariance and contravariance) have always been constraints that a types/delegates imposes on it itself. This is seemingly the correct place to put this.

As an aside: there is still an argument for where: maybeRefStructButMaybeNot because it could be seen as a variant of unmanaged (which is basically definitelyRefStruct).

@Gnbrkm41
Copy link

Gnbrkm41 commented Feb 7, 2020

While we're at it.... could we perhaps allow pointer types to be passed when T is constrained to be ref struct?

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 ref structs disallow boxing of instances I think it should be okay to allow doing so from the language perspective.

While it does not solve all the problems (pointers are allowed on heap/regular fields, whereas ref structs can't) this certainly would solve some of the problems.

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.

@benaadams
Copy link
Member

While we're at it.... could we perhaps allow pointer types to be passed when T is constrained to be ref struct?

Aside: you can do that with the unmanaged constraint (struct that contains no references)

public unsafe void M<T>(T* value)
	where T : unmanaged
{
}

@Gnbrkm41
Copy link

Gnbrkm41 commented Feb 8, 2020

Not quite. I meant something like Example<int*> where the method definition is void Example<T>(T value).

@xoofx
Copy link
Member

xoofx commented Feb 8, 2020

Not quite. I meant something like Example<int*> where the method definition is void Example(T value).

You can't, because pointers are not objects (unlike other primitives and structs), so they can't be boxed: object Example<T>(T value) { return (object)value; } is just not possible with a pointer.

@timcassell
Copy link

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)

@IS4Code
Copy link

IS4Code commented Dec 1, 2022

Thinking about this more, I agree that ref T as a parameter is the best way to express this, because it matches perfectly what in and out already mean in the same position, and is intuitive in the way it affects the code. Think of it this way:

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 ref T:

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 in and out constitute a promise that nothing inside the type would break their specific restrictions, ref is also a promise that every method inside the type is safe when a ref struct is passed. Here I mean code written as part of the type itself (C#, CIL or whatever), but not code in derived types (more about them later).

This also means that it should be perfectly safe to add ref to almost every delegate and interface in the BCL, without breaking anything. Just like it was IEnumerable<T> once, "upgrading" it to IEnumerable<out T> was backwards-compatible since it only gave more options to its users, without impairing the implementers of the interface.

As far as I can tell, this would prevent any sort of duplication of interfaces or delegates, since adding ref should be backwards-compatible too. So why not make these changes in .NET together with this?

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 IEnumerable<T> since adding ref only allows more things to do with it, not less, and it also doesn't affect any implementer (like List<T> : IEnumerable<T>) because if a generic parameter is not marked ref there, all is the same as usual.

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:

  • ref T is a backwards-compatible promise that the type only uses T in situations which are safe for a ref struct (or any ref-like type, perhaps including references in the future if they are allowed as members).
  • Adding ref to T does not impair any prior user of the type, since T still allows any type to be provided and if a non-ref type is provided, nothing special happens.
  • Adding ref to T does not impair any implementer, since they would also have to add ref to "opt-in" to ref-safety, and default to non-ref-safe. In other words, ref T does not "spread", it only limits what the type can do inside itself.
  • Removing ref from T is a breaking change, like removing in or out is (but perhaps worse). Adding it is not.
  • For these reasons, ref shouldn't be added automatically even when applicable, because it is a promise like in and out, stating both the capabilities and the intentions.
  • Constraints can still be added on T like usual, so something like ref T where T : IDisposable should work, even though ref structs would have to be allowed to implement interfaces in order for that to work (why can't they anyway? Boxing would still be prevented).
  • I don't see any need of also having a separate where T : ref struct constraint that would actually limit T to be a ref struct. Is there a situation where this would be necessary? I think ref T where T : struct makes sense, but ref structs don't give the user any more options in general by themselves.

In the future, I can imagine all of this working (but perhaps not all from the same point in the future):

  • IEnumerable<Span<byte>>, Func<Span<char>, Span<byte>> because that's what people want the most.
  • IEnumerable<TypedReference> Func<RuntimeArgumentHandle, TypedReference> because TypedReference finally deserves some love.
  • IEnumerable<byte*>, Func<char*, byte*>. Anything you can't do with a pointer (box it) also cannot be done with a ref struct.
  • IEnumerable<ref byte>, Func<ref char, ref byte>. Perhaps a long stretch, but if references are possible as members of ref structs and are permitted to be reassigned, they could be treated as ref-like values in this context and thus allowable too. I can imagine List<T> implementing IEnumerable<ref T> in the future, for example, since all you need is to implement ref T IEnumerator<ref T>.Current and List<T> could do that pretty well:
     var list = new List<int> { 1, 2, 3 }; // also implements IEnumerable<ref int>
     foreach(ref int i in list)
     {
         i += 1;
     }
     // now list is { 2, 3, 4 }
    Now ain't that nice? 😉

@timcassell
Copy link

timcassell commented Dec 1, 2022

@IllidanS4 I like the syntax, but you're wrong about IEnumerable and delegates. Existing implementers today can escape the values. The whole point of ref struct is that it cannot be escaped. That is not a backwards compatible change.

@IS4Code
Copy link

IS4Code commented Dec 1, 2022

@IllidanS4 I like the syntax, but you're wrong about IEnumerable and delegates. Existing implementers today can escape the values. The whole point of ref struct is that it cannot be escaped. That is not a backwards compatible change.

@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 interface IEnumerable { ... }, or what's inside the code for a delegate). Or I may be mistaken, but in that case, please show me a situation where this could happen. If you have List<T> : IEnumerable<T> and IEnumerable is defined as IEnumerable<ref T>, you cannot create List<Span<byte>> because it doesn't have ref T on itself, and you cannot cast it to IEnumerable<Span<byte>> because that interface can never be implemented by List<T>. The implementer (List<T> in this case) would have to mark its own T as ref, but then it restricts it like expected, disallowing instances to escape.

@timcassell
Copy link

@IllidanS4 Oh I see, I retract my statement.

@333fred
Copy link
Member

333fred commented Dec 1, 2022

@IllidanS4 remember that IEnumerable<T> implements IEnumerable. This wouldn't work for ref struct types.

@IS4Code
Copy link

IS4Code commented Dec 1, 2022

@IllidanS4 remember that IEnumerable<T> implements IEnumerable. This wouldn't work for ref struct types.

@333fred It would work just fine, you just couldn't implement it by returning the same thing like for IEnumerable<T>.

Say you have a class that implements IEnumerable<ReadOnlySpan<byte>>. Well, you can always implement IEnumerable by returning something different, like byte[] instead of ReadOnlySpan<byte>. Or just throw new NotSupportedException(), which is not the best but nobody uses non-generic IEnumerable anyway (and definitely not those who care about things like Span<T>). Sure, there are situations where you couldn't implement it in a satisfiable way, but that doesn't cause anything unsafe, just the inability to use certain (ancient) APIs.

In the case of List<T> implementing both IEnumerable<T> and IEnumerable<ref T>, it's again easy ‒ List<T> already implements IEnumerable, so there is no issue here too.

The only situation where this could pose an issue would be if IEnumerator<T> decided to provide a default implementation for IEnumerator.Current at some point in the future. Well it simply couldn't do that if it had ref T since it would involve boxing. But that is not an issue now since there is no default implementation (and they would be problematic to add anyway), and even if that becomes desirable, it can still be fixed in some way (like by marking specific methods "non ref-safe", effectively hiding the default implementation if T is a potentially ref-like type).

@atynagano
Copy link

atynagano commented Dec 2, 2022

@IllidanS4
It doesn't seem to make sense to add ref to the type argument of the interface.
While in and out constrain the signature of the interface method, what ref constrains is boxing in its implementation. Therefore, there should be no reason to distinguish between interface with T and interface with ref T, and if <ref T> means that T is compatible with the ref struct, then it should simply be written as follows.

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());

@IS4Code
Copy link

IS4Code commented Dec 2, 2022

@IllidanS4 It doesn't seem to make sense to add ref to the type argument of the interface. While in and out constrain the signature of the interface method, what ref constrains is boxing in its implementation. Therefore, there should be no reason to distinguish between interface with T and interface with ref T, and if <ref T> means that T is compatible with the ref struct, then it should simply be written as follows.

@atynagano It would be nice to be able to use an interface with ref struct without adding ref, but how would the language determine that it is safe?

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 ref struct, while with ref T, the compiler would check that it behaves well, and thus the implementation would be available as usual. Compare it with readonly on structs for example ‒ yes, the compiler could check that a struct can be implicitly readonly, but a promise shouldn't be implicit if you could later break it without even realizing.

Also it doesn't seem much consistent to me if you don't want ref T on interfaces, but allow it everywhere else: methods, classes, structs, and delegates. Note that ref-safety is not determined solely by the implementation:

interface Interface4<T>
{
    T Method(Class<T> arg);
}

Here, based on what you imply, the ref-safety of T would be determined by whether Class is defined as Class<T> or Class<ref T>. I don't like the possibility that changing a class that might be only used once among lots of parameters in one method amongst many would change the whole interface, without you possibly noticing (note that the behaviour of ref T would again be consistent here with in T or out T, as it also imposes a requirement on the usage of T as a type argument).

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 in and out.

@timcassell
Copy link

timcassell commented Dec 2, 2022

@IllidanS4 It doesn't seem to make sense to add ref to the type argument of the interface. While in and out constrain the signature of the interface method, what ref constrains is boxing in its implementation. Therefore, there should be no reason to distinguish between interface with T and interface with ref T, and if <ref T> means that T is compatible with the ref struct, then it should simply be written as follows.

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());

I think for that case, the compiler could just not implicitly implement IEnumerator.Current if the return value of Current is incompatible, and force the implementer to implement it explicitly. I don't think it breaks the IEnumerator<ref T> interface in any way.

And as for the IEnumerator<object> covariance, that is not actually covariant, and would be an illegal assignment. in and out for ref structs means nothing, as they cannot be converted to any other type.

Like @IllidanS4 said, the compiler needs to know if the T is allowed to be a ref struct to impose limitations on the implementer. The LDM mentioned that we're actually talking about an anti-constraint rather than a constraint. They mentioned adding an allow keyword that can sit next to where, but I think this <ref T> syntax is much cleaner (and there are very few possible anti-constraints aside from ref structs).

@atynagano
Copy link

@IllidanS4

interface Interface4<T>
{
    T Method(Class<T> arg);
}

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 <ref T> syntax.

@atynagano
Copy link

For reference, rust language attaches Sized constraints(bounds) to most types, and type arguments are assumed to be Sized by default. To loosen this constraint, you must explicitly specify where T: ?Sized. To follow this syntax, it become where T : ?boxable...?

@irvnriir
Copy link

irvnriir commented Oct 7, 2023

related: Unsafe.SkipInit to support ref struct items #7581

@jaredpar
Copy link
Member

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.

#7608

Thanks for getting the convo started here and everyone for the feedback

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