csharplang
csharplang copied to clipboard
[Proposal]: Self Constraint
Self constraint for generic type parameters
- [x] Proposed
- [ ] Prototype: Not Started
- [ ] Implementation: Not Started
- [ ] Specification: In progress: https://github.com/dotnet/csharplang/blob/main/proposals/self-constraint.md
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
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.
It would be useful to me to have this on classes, too.
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
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
}
}
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.
@fabianoliver this
is perhaps not the best name choice, as it usually means "current instance" and not "current type".
@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
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.
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.
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!
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 (https://github.com/dotnet/csharplang/issues/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?
[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
?
@Qiu233 It's a temporary solution
(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>
{
}
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.
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) { ... }
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.
Would there be an issue with doing the Rust thing of having
This
refer to the type ofthis
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?
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!