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

[Proposal]: Self Constraint #5413

Open
1 of 4 tasks
tannergooding opened this issue Nov 11, 2021 · 22 comments
Open
1 of 4 tasks

[Proposal]: Self Constraint #5413

tannergooding opened this issue Nov 11, 2021 · 22 comments

Comments

@tannergooding
Copy link
Member

tannergooding commented Nov 11, 2021

Self constraint for generic type parameters

Design meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-10.md
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts

@TahirAhmadov
Copy link

I would say the second option is better, where T : this, and this should be available on classes, too - imagine a similar scenario for an abstract class.

@jnm2
Copy link
Contributor

jnm2 commented Nov 15, 2021

It would be useful to me to have this on classes, too.

@0x0737
Copy link

0x0737 commented Nov 16, 2021

The best way this could be done is to allow it on classes and make it an associated type to prevent pollution of type parameters at the user side

@TahirAhmadov
Copy link

Another thing I just noticed in my own work, imagine a builder class like below:

class Base<TSelf> where TSelf : Base<TSelf>
{
  public TSelf SetOption()
  {
    ...
    return this; // right now this is an error
    return (TSelf)this; // this has to be done
  }
}
// can we make it work like below?
class Base<TSelf> where TSelf : this
{
  public TSelf SetOption()
  {
    ...
    return this; // works
  }
}

@fabianoliver
Copy link

Great proposal.

Tiny suggestion: It might be nice if we could reference a self-type in implementing type declarations without having to re-state the type name explicitly. E.g.

interface IBuilder<this TSelf>
{ ... }

class SomeBuilderWithAnAnnoyinglyLongAndComplicatedName : IBuilder<SomeBuilderWithAnAnnoyinglyLongAndComplicatedName>
{ ... }

becomes

class SomeBuilderWithAnAnnoyinglyLongAndComplicatedName : IBuilder<this>
{ ... }

That might improve code readability & brevity (I'd think most class names are >4 characters long) a fair bit.

@vladd
Copy link

vladd commented Dec 9, 2021

@fabianoliver this is perhaps not the best name choice, as it usually means "current instance" and not "current type".

@fabianoliver
Copy link

fabianoliver commented Dec 9, 2021

@fabianoliver this is perhaps not the best name choice, as it usually means "current instance" and not "current type".

"This" seemed like a natural fit if the proposal also uses it as a qualifier for the self generic type (i.e. IBuilder<this TSelf>), in which case we have precedent for "this" referring to a type indeed. It would seem like a natural fit in this case (and also would avoid adding extra keywords to the language).

Having said that, personally I wouldn't be too hung about about the specific terminology, another reserved identifier would be fine as well

@MadsTorgersen
Copy link
Contributor

I've been thinking about self-types quite a bit recently, and tl;dr, I don't think this is how we should do them. I think that self-types are better expressed in a framework of associated types (aka existential types #5556, #1328, aka abstract types, aka virtual types) or similar, primarily for the reason that @0x0737 mentions above: not polluting a type with extra type parameters. However, it is possible that we can come up with a notion of "self-constraint" that would apply orthogonally to both type parameters (now) and associated types (if we ever get them), so we may not have to wait for that.

If we did such a self-type/self-constraint feature (on top of current generics and/or future associated types) we would want it to be expressive across the language - in classes as well as interfaces. And we would want examples like @TahirAhmadov's above to work, i.e., this is assignable to any self-type:

class Base<TSelf> where TSelf : this
{
  public TSelf SetOption()
  {
    ...
    return this; // works
  }
}

It's possible that we can come up with such a proposal, but this isn't it. I worry about adopting a notion of self-types that only serves a relative corner scenario (static virtual members, which don't even have a this value). I think we risk "burning" the notion of self-types where we should be saving it for something more valuable and encompassing.

@333fred
Copy link
Member

333fred commented Mar 29, 2022

This was discussed in LDM: https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts

While we reject this version of the self constraint, that's not to say we're uninterested in self types in general, we just think it should be an existential type, not a constraint on a visible type parameter.

@craigajohnson
Copy link

An enthusiastic +1 for this concept. Static abstract interfaces is a (somewhat) useful waypoint on the path to TSelf.

C# is too great of a language not to solve CRTP!

@alrz
Copy link
Member

alrz commented Apr 10, 2022

However, it is possible that we can come up with a notion of "self-constraint" that would apply orthogonally to both type parameters (now) and associated types (if we ever get them), so we may not have to wait for that.

A self-type constraint works for broader scenarios, but it seems to me that it could be introduced implicitly as an associate type with a new type kind. eg trait INumeric { /*TSelf in scope*/ }

That said, there's a few questions for the stopgap attribute proposal (#6000):

  • Should it enforce an ordering (must be the first type parameter?)
  • Should it enforce the type parameter to be invariant?
  • Is it possible to use the new kind for existing types without breaking code? (eg IEquatable<T>)

Mentioned the last point because I think [SelfType] is going to be applied to IEquatable, maybe?

@Qiu233
Copy link

Qiu233 commented Apr 11, 2022

[SelfType] attribute is a good idea.
But it might be better to have a more dedicated syntax for this in C# grammar, just like nint for [NativeInteger] IntPtr?

@0x0737
Copy link

0x0737 commented Apr 11, 2022

@Qiu233 It's a temporary solution

@zms9110750
Copy link

zms9110750 commented May 20, 2022

(machine translation)
I read an article saying that not everything is inherited from object. Interface is one of them.
But when I declare an empty interface type, the IDE still tells me that I can call the toString method.
I think this is because the IDE can determine that it inherits from object, even if it doesn't know the content of the implementation class.
Then, this feature should be applied to Self Constraint.

Bar<Derive> bar = new Derive();
bar.FooMethod();//This should be valid
Foo foo = bar ;//This should be valid

class Foo
{
	public void FooMethod() { }
}
interface Bar<T> where T :this, Foo
{
}
class Derive :Foo, Bar<Derive>
{
}

@333fred
Copy link
Member

333fred commented May 20, 2022

Yes, that's an excellent example of the type of thing that needs more thought before we have a self or this constraint in the language.

@TahirAhmadov
Copy link

I just came across another place where this would be useful, it would be cool to have things like below:

public delegate void TypedEventHandler<TSender, TArgs>(TSender sender, TArgs args);
abstract class Base<TSelf> where TSelf : this // or "self" or w/e syntax we choose
{
  public event TypedEventHandler<TSelf, EventArgs>? SomeEvent;
}
class Child : Base<Child>
{
}
var child = new Child();
child.SomeEvent += child_SomeEvent;
void child_SomeEvent(Child sender, EventArgs e) { ... }

I also realized we may have a problem:

class GrandChild : Child
{
}
var grandChild = new GrandChild();
grandChild.SomeEvent += grandChild_SomeEvent; // this wouldn't work, would it?
void grandChild_SomeEvent(GrandChild sender, EventArgs e) { ... }

@jamescarterbellMSFT
Copy link

jamescarterbellMSFT commented May 19, 2023

Would there be an issue with doing the Rust thing of having This refer to the type of this in the type system?

Taking TahirAhmadov's example:

class Base<TSelf> where TSelf : Base<TSelf>
{
  public TSelf SetOption()
  {
    ...
    return this; // right now this is an error
    return (TSelf)this; // this has to be done
  }
}
// can we make it work like below?
class Base
{
  public This SetOption()
  {
    ...
    return this; // works
  }
}

*Oops, totally missed where this was suggested above.

@HaloFour
Copy link
Contributor

Would there be an issue with doing the Rust thing of having This refer to the type of this in the type system?

If it was going to use a keyword like that why not just use this which is already reserved and couldn't collide with any existing types already called This?

Either way, I don't think the syntax is the problem there. What would that actually compile to in IL? How would it be consumed?

@claudiudc
Copy link

For years I wish this feature to be available, I hope one day will do. I do always implement all kind of unpleasant workarounds. For me it is one of the most important features that I would like to see it available into C#.
Guys, please make this available! It is really valuable feature!

@zms9110750
Copy link

Can we use Self Constraint to write this?

public class Delegate
{
public virtual T[] GetInvocationList(); where T : this
}

@HaloFour
Copy link
Contributor

HaloFour commented Oct 6, 2024

Can we use Self Constraint to write this?

You'd need to declare GetInvocationList to be generic, otherwise there's no generic type parameter to constrain.

@zms9110750
Copy link

You'd need to declare GetInvocationList to be generic, otherwise there's no generic type parameter to constrain.

Oh, yes, I forgot.
What I mean is, can we write a self constrained generic method in a non generic class.

As you can see, this method requires us to manually filter out the corresponding types.
An extended generic method can solve this problem.
But it would be great if he could solve this problem on his own.

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