Replies: 28 comments 1 reply
-
There is another suggestion to allow any expression in public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(nameof(Name), typeof(Name), typeof(this)); But I think using |
Beta Was this translation helpful? Give feedback.
-
Cheekily plugging my own similar proposal in #169, that achieves all of this (though isn't implicit on all classes by default, perhaps it could be) through a It's not immediately clear to me whether this proposal adds any significant extra capabilities that #169 doesn't, or vice versa. #169 is limited to abstract classes and interfaces, that seems like the most significant difference. I suspect that #169 is much easier to implement as it's basically just a little compiler check on top of existing generics functionality, but this arguably provides a slightly nicer API for developers. #253 was also just created and references a version of #169 mixed with defaults that brings the APIs a little closer together. |
Beta Was this translation helpful? Give feedback.
-
MyType is a really nice feature that I wish many more languages would have. A lot of the gymnastics people do with F-bounded polymorphism are mainly to emulate a MyType feature. I.e. you are forced to write something like this: interface ICloneable<out T> where T : ICloneable<T>
{
T Clone();
}
class MyObject : ICloneable<MyObject>
{
public MyObject Clone() => new MyObject();
}
// unfortunately, this is not type-safe:
class MyEvilObject : ICloneable<MyObject>
{
public MyObject Clone() => new MyObject();
} instead of interface ICloneable
{
this Clone() { /* … stuff … */ }
}
class MyEvilObject : ICloneable
{
public MyObject Clone() => new MyObject();
// Error: there is no implicit reference conversion from 'MyObject' to 'MyEvilObject'.
} MyTypes are discussed in Foundations of Object-oriented Languages: Types and Semantics by Kim B. Bruce. They exist in Swift, unfortunately, the Language Reference is rather silent on them: there is only a single sentence in the section on Protocol Associated Type Declaration in the entire 1100 page book which reads:
[Thanks to @Mafii, who pointed this out in his comment.] Rust has Self-Types. [Thanks to @whoisj, who pointed that out in his comment.] D has Template Scala has self-type annotations, which are different: they allow you to explicitly annotate |
Beta Was this translation helpful? Give feedback.
-
IIRC there is a self type in Swift in Protocols, this could be used as a common ground for this idea. |
Beta Was this translation helpful? Give feedback.
-
@TheOtherSamP: I also thought of a type constraint alone first, but then I saw some issues depending on the use case with that -- it only solves part of the problem: IClonable<T> where T : this {...}
MyBaseClonable : IClonable<MyBaseClonable> {...}
MyDerivedClonable : MyBaseClonable {...}
Even the first one won't work that easy, let's try: IClonable<T> where T : this {...}
MyBaseClonable<T> : IClonable<T> where T : this{...}
MyDerivedClonable<T> : MyBaseClonable<T> where T : this {...} The Class Model itself looks good, but I have a major problem: I can't create any instances: var myclonable = new MyDerivedClonable<T>(); // T is unspecified
var myclonable = new MyDerivedClonable<MyDerivedClonable>(); // MyDerivedClonable is undefined
var myclonable = new MyDerivedClonable<MyDerivedClonable<MyDerivedClonable<... infinite recursion ...>>>(); // I can't even specify a variable type without first requiring a non-generic derived type -- which is not what I want, since then the other derived generic types wouldn't inherit from that type anymore. As far as I can see, this issue might only be solved with a special generic type var myclonable = new MyDerivedClonable</* <--- that type over there. */>(); // needs a keyword. And since we can automatcially determine whether using this keyword is required, I thought it would be best to just infer it from context, without forcing people to write it out, hence the suggestion above. Omitting the keywords also helps to require only one keyword for my suggestion, by not requiring to differenciate between With only type constraints, I also can't generate new Objects of the type |
Beta Was this translation helpful? Give feedback.
-
@alrz : I don't think I would use it a lot, but it feels inconsistent not to allow the usage of the public void CopySettingsFrom(this template); I'm not yet sure whether |
Beta Was this translation helpful? Give feedback.
-
I'm not sure I entirely understand what you're getting at here to be honest. IClonable<T> where T : this {...}
MyBaseClonable<T> : IClonable<T> where T : this{...}
MyDerivedClonable<T> : MyBaseClonable<T> where T : this {...} I would have implemented this as: interface IClonable<T> where T : this {...}
abstract class MyBaseClonable<T> : IClonable<T> where T : this{...}
class MyDerivedClonable : MyBaseClonable<MyDerivedClonable > where T : this {...} This obviously has the restriction that MyBaseClonable is required to be abstract. Is this the point that you're making? If so, then yes, I absolutely agree that that is an acknowledged limitation under my proposal, for the infinite recursion reason you demonstrated. I do agree that this limitation is not ideal, and that some way of being able to resolve that recursion would be great. I view my proposal as broadly including the method of implementation, and yours as just being the API you'd like to see. I agree that your API has benefits relative to mine and would probably be preferable, but I don't know how much of a chance it has of actually being implemented any time soon. The strongest argument I would still make in favour of my implementation is that it's a much smaller change. While ideally the constraint would be implemented in the CLR, it could be approximated pretty well within just C# itself. The compiler could place an attribute on the type parameters for enforcing it internally, and inject a runtime check into the static constructor for interfacing with other languages. Plugging #255 where I asked about the status of the CLR with this debate in mind. |
Beta Was this translation helpful? Give feedback.
-
No, not really. My lack of interest in a reduced version mainly consists of two parts:
1: I can't handle the type: Imho, making the class abstract doesn't help -- it's just another way of storing the information that it can't be instanciated. Not only that I fail to create an Instance, neither can I define a variable, which is what I also usually want to do at some point when defining interfaces.: // another kind of clonable
interface IClonable<T> : IClonable<T>, ... some other features ...
where T : this
{}
// definitely no generic class
// i need to be able to instanciate it without knowing what type will be stored insite
class DeepCloneCyclicGraphManager {
// IDerivedClonableInterface<SomeConcreteValue> is too specific
// IDerivedClonableInterface<T> does not work, we have no type parameter
// IDerivedClonableInterface<object> might not support structs, and will even fail when using covariance, since Object does not derive from IClonable.
// IDerivedClonableInterface<?> is a Java feature, which does not exist in C#, as far as I know
public IDerivedClonableInterface</*???*/> WrappedElement { get; set; }
} Possible workarounds:
2: My class structures often are infinitly extendable If there is no reason to make a class abstract, because no abstract functionality is required, I usually don't do it. Some classes just provide some protected and/or virtual methods and events where derived classes can hook in, but the base class itself is functional and can be used standalone. I don't want to give this up, where the architecture does not make this necessary.
In such a situation, I would usually make MyDerivedClonable sealed -- but usually, I want to be able to further derive from it, just to avoid situations where a new requirement requires "one special element" that does change a minor implementation detail. Using abstract classes, it would look like this:
I think this will cause more bugs than it prevents -- every subtype returns the correct Clone() result type, but other people will first have to understand that MyConcreteSpecialDerivedClonable is not a MyConcreteDerivedClonable, and wonder why.
Do we already have such features? From what I've read -- sorry, no sources, just my overall impression from the different roslyn issues -- I doubt the "compiler check" will be introduced as a language feature without a CLR check. Linq-Syntax doesn't influcence other CLR languages (VB.net, IronPython), extension methods simply are only static methods in other languages, and cannot be defined there -- these are features that don't require a CLR change, because they can not break anything. The compiler check could also be implemented with attributes and some postcompiler like Fody -- it probably wouldn't use much more code, an attribute (does AttributeTargets.GenericParameter fit?) on the parameter itself might be enough to check this in a postcompiler. The problem is: As soon as someone with VB.Net instanciates any of these classes without caring about the type constraint, and passes the instance to C# code, which assumes correct behaviour (we have a type constraint!), I expect the implementation to fail horribly -- and someone has to sit there a long time until one can figure out what the actual problem is -- the runtime not honoring C# type constraints. |
Beta Was this translation helpful? Give feedback.
-
PS: The static constructor check does seem possible, but I don't think of it as a nice solution. Also, this would also be possible when using the feature as suggested in my syntax -- the implementation of my features above also describe a generic type parameter with a specific constraint; but since I suspect that this might have minor Performance impacts and might prevent compiler optimizations, so I would prefer a CLR-Implementation, but one can implement almost everything without changing the CLR if the classes themselves refuse to load. The constraints then would have to be compiled as Attributes. |
Beta Was this translation helpful? Give feedback.
-
With regards to point 2, I think this ultimately comes down to the abstract restriction. Ultimately it's all about the restrictions of the infinite recursion. We're largely in agreement here, if we can get a version of this feature that solves that problem that would be absolutely fantastic, I just have doubts about practical viability for now. Perhaps I'm just being a pessimist. With regards to point 1, we again somewhat agree. I think many situations could be worked around with slightly different patterns and interface variance, but it would be cumbersome to have to do so relative to a solution that magics all of that away. The situations where I've found myself wanting this myself have largely not come up against these scenarios, but I can absolutely see that it would be a frustrating limitation for many. A fix for this would probably be essential for moving this from the area of a niche feature for those occasional awesome APIs when there's no other way, into something people can happily throw around with little care as a sprinkle of niceness everywhere. I have half of an idea for how this restriction could be worked around nicely actually, I'll think about it and get back to you here if that idea actually turns out to work, though I strongly suspect it won't. As for the static constructor check, I was imagining that it could perhaps be implemented in such a way that it could be replaced by a CLR constraint at a later date whenever CLR stuff finally happens, and the runtime check could be a temporary thing. I don't know how feasible that is. Other major languages could also independently implement features that respect the attributes without making changes to the CLR, so the problem of dealing with the runtime checks may be more theoretical than practical. Ultimately, if we can actually get all of the features of your way that would be great, I'd love that! |
Beta Was this translation helpful? Give feedback.
-
One other thought on this: Omitting specifying the implicit type parameter is necessary to avoid infinite source code recursion, but naming might still be necessary. Multiple this type parameters on the same class don't make any sense, but there might be multiple parameters in scope when nesting classes: Maybe something abstract class ClonableList<TElement> : IClonable
where TElement : ClonableList<TElement>.ClonableListElement
{
abstract class ClonableListElement : IClonable
{
virtual thistype(ClonableList<TElement>.ClonableListElement) Clone() { ... }
thistype(ClonableList<TElement>) ParentList {get; set;} // would not be possible, since "thistype" alone would reference ClonableListElement, which is the wrong type.
}
virtual thistype(ClonableList<TElement>) Clone() { ... }
TElement FirstElement {get; set;}
TElement LastElement {get; set;}
} Or simply name the parameter, but prevent filling it in manually: // two parameters on declaration:
abstract class ClonableList<thistype TThis1, TElement> : IClonable
// one parameter anywhere else, we do not want to write ClonableList<ClonableList<..., TElement>, TElement>
where TElement : ClonableList<TElement>.ClonableListElement
{
abstract class ClonableListElement<thistype TThis2> : IClonable
{
virtual TThis2 Clone() { ... }
TThis1 ParentList {get; set;}
}
virtual TThis1 Clone() { ... }
TElement FirstElement {get; set;}
TElement LastElement {get; set;}
} Any opinions on this? |
Beta Was this translation helpful? Give feedback.
-
Simply put, today we cannot Given the success Rustlang has been having with |
Beta Was this translation helpful? Give feedback.
-
Good spot on the losing access with nesting. Perhaps this would be a good time to introduce something else I've considered before, and allow |
Beta Was this translation helpful? Give feedback.
-
phi1010 thank you for opening this subject, for me it is one of the most important features that I would like to see it available into C#. |
Beta Was this translation helpful? Give feedback.
-
Another potential plus point for supporting a "this" type is it could potentially be used to avoid boxing issues when using value types. For example interfaces that are implicitly generic on "this" type could be implemented with methods that take "this" as a (hidden/implied?) parameter and call methods on it without the need for boxing. |
Beta Was this translation helpful? Give feedback.
-
There is another use case where "this" type would be very helpful: public class Node
{
public thistype Next { get; set; }
public thistype Previous { get; set; }
}
public class TextNode : Node
{
public string Text{ get; set; }
private void CheckText()
{
if (Next.Text== "abc")
{
// Do something
}
}
}
public class TextValueNode : TextNode
{
public int Value { get; set; }
private void CheckValue()
{
if ((Next.Text == "123") && (Next.Value == 123))
{
// Do something
}
}
} CRTP wouldn't work if you use it in the middle of your implementation. In below example public class Node<T> where T : Node<T>
{
public T Next { get; set; }
public T Previous { get; set; }
}
public class TextNode : Node<TextNode>
{
public string Text{ get; set; }
private void CheckText()
{
if (Next.Text == "abc")
{
// Do something
}
}
}
public class TextValueNode : TextNode
{
public int Value { get; set; }
private void CheckValue()
{
if ((Next.Text== "123") && (Next.Value == 123))
{
// Compile error CS1061 'TextNode' does not contain a definition for 'Value'
}
}
} |
Beta Was this translation helpful? Give feedback.
-
@KubaSzostak With your example, if you had: Node node = new TextNode(); What would be the type of I see the OP goes to some length to address this, but it looks like your example is not consistent with this proposal? |
Beta Was this translation helpful? Give feedback.
-
This assignment would not work due to contravariance -- let's display the implicit type parameter here: class Node<in thistype> {...} // setter requires in, getter requires out, both is not useful under any circumstance.
Node<Node> node = (Node<Node>)(Node<TextNode>) new TextNode<TextNode>(); The cast from Contravariance can only work when re-implementing the every method here, thus providing new implementations of the property setter for every subclass -- which does not seem very useful for field-like properties. |
Beta Was this translation helpful? Give feedback.
-
In that case there should be CS0029 Error raised (Cannot implicitly convert type). The same way as this error is raised in other cases, eg.: List<Node> nodeList = new List<TextNode>();
List <FileSystemInfo> pathList= new List<DirectoryInfo>();
// Error CS0029 Cannot implicitly convert type 'System.Collections.Generic.List<System.IO.DirectoryInfo>' to 'System.Collections.Generic.List<System.IO.FileSystemInfo>' |
Beta Was this translation helpful? Give feedback.
-
You then violated: |
Beta Was this translation helpful? Give feedback.
-
@KubaSzostak So if |
Beta Was this translation helpful? Give feedback.
-
You're right. I see the problem now. Unfortunately I have no idea how to figure it out. |
Beta Was this translation helpful? Give feedback.
-
Seems maybe related to this: |
Beta Was this translation helpful? Give feedback.
-
@KubaSzostak : Your Node-Example rather looks to be a case for mixins -- which C# does not support either, but they maybe can be added with some Fody postcompilation magic. Inheritance usually intends two objects to have a similarity, by being exchangeable in some situations -- however, the nodes of linked lists of different types are not intended to have any compatibility, only reused code. Mixins, or extension methods might suit your usecase better. You don't even need this proposal, it does work without: abstract class Node<TNode> { TNode Next { get; set; } /* plus some list iteration functions, maybe */ }
class TextValueNode : Node<TextValueNode> { } |
Beta Was this translation helpful? Give feedback.
-
@Acerby : What you linked is a technical detail required to implement this, although it doesn't become obviously visible here when looking at the code, since it would be hidden behind some kind of keyword or type parameter. |
Beta Was this translation helpful? Give feedback.
-
A more general solution for this seems to be #2936. |
Beta Was this translation helpful? Give feedback.
-
I've always loved this idea. However, I believe that using It should do exactly that but without the ambiguity of extension methods or in any other context. |
Beta Was this translation helpful? Give feedback.
-
I just hope that the self-typed generic parameter won't be required to be written always. It would pollute code very much. Omitting the parameter like in this proposal is must-have for this feature |
Beta Was this translation helpful? Give feedback.
-
History
I'd like to revive an issues from the roslyn repo:
https://github.com/dotnet/roslyn/issues/13988
Motivation
I'd like to declare interfaces allowing me to provide meta-functionality operating on types, such as (deep) cloning, converting, synchronizing, wrapping, subordinating, etc. generic classes. To make sure that
myobject.CloneThisOne()
actually returns a clone of the same type as the cloned object, we currently have to use runtime checks. It would be useful if the compiler could guarantee this.Two use-cases (IClonable-IClonable and IChild-IParent type relationship) are described in https://github.com/dotnet/roslyn/issues/13988
The following part might change over time, as I try to fully include https://github.com/dotnet/roslyn/issues/4332 without creating inconsistencies.
https://github.com/dotnet/roslyn/issues/4332
Suggestion
Keyword
We need a keyword that can be used instead of a type name, or instead or together with of the new(), struct or class type constraints. The earlier issues suggest the keywords "this", "subclassed", "concrete", "thistype". Since both suggestions use the keyword in different positions to create similar results, it makes sense to use the same one
The keyword "this" seems to be a good choice, since it does not introduce a new reserved word and has not yet any meaning when used in a Type context (e.g. in
where T : this
-- but also intypeof(this)
,new List<this>()
, orpublic this ReturnSomething()
out of scope for this suggestion). Using this instead of a type thus should not create any major confusion.Drawback:
The usage of "this" defined here might be confused with the use of "this" in extension methods. It can be distinguished by whether we look at a static or instance method:
"this" as generic type parameter on classes (not generic methods)
This part of the suggestion works on all of these types.
Interfaces are easy to define, since they do not implement any method bodies.
Also, sealed classes are easy to implement, since no derived classes can be expected as a return value.
Both non-sealed non-abstract classes may require special handling. To ensure every derived type implements the methods if necessary (e.g. cloning itself), we need to define abstract methods on non-abstract classes, which still need to have a method body.
Using normal method implementations with virtual or non-virtual methods still is possible, but may require some types to match.
Declaration
We want to use "this" as a special type parameter:
It is always an covariant parameter -- where the type
this
is used, any derived type will only return values or accept method parameters that are of its own type, thus, of a derived type.As syntactic sugar, we allow to omit writing the type parameter. If "this" is used as a type in the definition of the class, it can be implicitly assumed to be there:
Deriving from this-parametrized interfaces
This type parameter is mandatory to all derived classes. It can never be specified manually, thus, every derived class/interface/struct is generic. We do not specify the this parameter on the class that we derive from:
The compiler enforces IInterface2 to manifest a this parameter.
We also allow to omit the type parameter here under the same conditions, since its use already enforced when deriving from IInterface1 -- for readability, it still can be written out, if desirable
Deriving a non-abstract class from this-parametrized interfaces/classes
Any class deriving from a this-parametrized interface has ensure that the methods are implemented. We thus introduce abstract methods with a method body, which can only be executed on an instance of the defining class, not on an instance of a subclass.
Inside
Class3
, the implicitly casting any value of typeClass3
or any derived class to typethis
is allowed, if:Class3
is sealed, orDeriving another class
Any abstract methods are treated as such, we are forced to redefine the method body. Calling the abstract method on the base class is not allowed, since it has to be treated as if not defined on every instance of a subclass:
Calling
((Class3) myClass4).GetClone()
would result in a call tomyClass4.GetClone()
, since the method is virtual.Creating an instance
When creating an instance, the type parameter can not be specified, as this would require an infinite chain -- this is the initial problem, that only a
this
type can solve:It thus must be omitted:
Using
this
will result in an error:Using an instance
The "this" type allows generating typesafe "meta operations":
These are even valid in generic types:
"this" in a type constraint
We can use
this
in a type constraint:This can make sure that only compatible implementations are used:
And allows us to write generic code for cyclic references across multiple classes:
Local variables of type
this
Should be available -- the type has to be available for expressions anyways.
Generics of type
this
Might be possible where interfaces are returned, but it not necessary for the basic implementation. Has to ensure covariance/contravariance as interfaces do on both inputs and outputs, if the class is not sealed, even when the method is abstract. If the method would not be valid on an interface with the type parameter , it should not be able to be defined:
Fields of type
this
...Might be possible, but is not necessary for the basic implementation. The implicit casting rule would still apply. This means, we can set those fields:
new MyClass3()
. Since these methods can never be called on an instance of a derived class, they can never set the field to an instance of the wrong type.this
or the return value from any method returningthis
.Delegates with
this
parameters/closures and return valuesMight be possible, using the same covariance/contravariance-rules as anywhere else.
Consistency
Any method can be marked abstract to enforce every derived class to override it.
Why:
We need this feature on methods that return a value of type
this
to ensure a method is always properly implemented. Checking whether a method fit's this criteria is difficult, thus, we allow all methods to use this feature, even if it might not make sense to actually use this.Defining which method returns a value of a specific type might be difficult, since returning might happen:
IEnumerable<this>
instead ofthis
Action<this>
Func<this>
To avoid this complexity, any method can be abstract and still have a method body, which is only called on non-derived instances.
Only one "this" type parameter can be specified, there is no
<this T1, this T2, T3, T4>
Why:
The type parameter is always determined where the object is instanciated, and is always equal to the type of the object. Since two of these type parameters would always be equal, we don't need two or more of them, one without a name is enough.
Related
This might also solve the use-case of issue https://github.com/dotnet/roslyn/issues/311 ("this" type as return type), by marking the method mentioned in https://github.com/dotnet/roslyn/issues/311#issuecomment-73806696 as abstract, and still providing an implementation.
Beta Was this translation helpful? Give feedback.
All reactions