-
Notifications
You must be signed in to change notification settings - Fork 4k
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: support covariant return types #357
Comments
How would that work if a consumer called the virtual method on the base class given that the derived class can't both override and shadow the method? public class Foo
{
public virtual object Baz()
{
return "fizz";
}
}
public class Bar : Foo
{
public override string Baz()
{
return string.Concat(base.Baz(), "buzz");
}
} What would |
Yes- please please do this feature. As I'm sure you're all aware, this feature is far and away one of the most commonly asked about issues on StackOverflow and elsewhere. People expect this to "just work" and when it doesn't it throws a big wrench into the whole design of a factory pattern. Would be interested to see the details of the compiler implementation fleshed out a little further. |
@HaloFour although you cannot hide and override in source, the compiler can arrange to do that in IL. |
@gafter Okay, great. That makes perfect sense. |
This could work. In this example, the override returns the same type as the base method. The C# compiler generates a special attribute indicating the covariant return type. The C# compiler would use this attribute for semantic analysis (such as a method which overrides Callers (in other assemblies) using earlier versions of C# have no problem consuming these APIs; they just have to insert the casts themselves. Source (C# "vNextNext"):public class Foo
{
public virtual object Baz()
{
return "fizz";
}
}
public class Bar : Foo
{
public override string Baz()
{
return string.Concat(base.Baz(), "buzz");
}
}
public class Program
{
public static void Main(string[] args)
{
Bar bar = new Bar();
string result = bar.Baz();
Console.WriteLine(result);
}
} Result (C# "vNow"):public class Foo
{
public virtual object Baz()
{
return "fizz";
}
}
public class Bar : Foo
{
[return: System.Runtime.CompilerServices.ReturnType(typeof(string))]
public override object Baz()
{
return string.Concat(base.Baz(), "buzz");
}
}
public class Program
{
public static void Main(string[] args)
{
Bar bar = new Bar();
string result = (string)bar.Baz();
Console.WriteLine(result);
}
} |
@sharwell Why the need for an attribute? That would require an addition to the CLR for something that I imagine won't be implemented for C# 6.0 "vNow"/"vNext" anyway. Why not just solve it "more correctly" in C# 7.0 "vNextNext" by having the compiler emit both the overriding method and the shadowing method with the same name and parameters? Then you don't have a type-erasure problem and any existing compiler would already support binding to the correct overload. That would allow the language support for this feature to be supported on literally any framework version and even by older versions of the compilers.
public class Bar : Foo
{
public override object Baz()
{
return this.Baz();
}
public new string Baz() // not legal C#, perfectly legal in IL
{
return string.Concat(base.Baz(), "buzz");
}
} The compiler would then bind calls to |
@HaloFour A custom attribute would allow the compilers for multiple languages that compile to IL to provide and consume this feature to any other language capable of expressing this concept. A prime example is the
|
Furthermore, using shadowing would allow a covariant return to be a value Lastly, this compiler candy would work regardless of which framework
|
@HaloFour So we are clear, your points are presented well even if I do not draw the same conclusion at this time. I appreciate that you are challenging me really think about the position I've taken and whether or not it would work in the long run. Several features of C# and other .NET languages are supported by custom attributes, and this list grows over time. Another prime example is After working through the following example, I believe the concerns 1 and 2 would be handled equally well with your proposal. Consider three classes:
Do you have an example where the .NET framework uses shadowing when overriding a method defined in a base type? |
@sharwell Thanks, although I'm really just arguing the point for @gafter since this is the implementation as he described it. I have no examples of where .NET does this currently. I'm not aware of any language other than IL that permits this to be expressed. I do think that it's proper if class Playing with it a little more I am running into issues with ambiguity. C# can handle a single level of inheritance just fine. VB.NET is even worse in that it cannot resolve If those issues cannot be resolved in a manner that would be backward compatible that would effectively limit the functionality to projects using the post-Roslyn compilers where presumably the overload resolution would be modified to handle these situations. I'm going to play with this a little more. I'd like to hear from @gafter regarding this problem. Update: Update 2: |
@sharwell To ping the thread, using explicit overrides to hide the base member through changing name/visibility fixes the overload resolution problems I was experiencing with C# and VB.NET. You effectively end up with the following: public class Foo {
public virtual object M() {
return "fizz";
}
}
public class Bar : Foo {
// sorta equivalent to explicit implementation except for overriding, which is legal in IL
private sealed override object Foo.M() {
return this.M();
}
public new virtual string M() {
return string.Concat(base.M(), "buzz");
}
} Compilers looking at |
Any issue with using Certain other downsides remain:
|
Summary so far Shadow methodAdvantages:
Disadvantages:
Attribute applied to return typeAdvantages:
Disadvantages:
|
@sharwell Looks like 1.. If the compiler emitted a third private non-virtual method which contained the actual method logic that would probably be a little more efficient, especially if the compiler emitted a public class Bar : Foo {
// sorta equivalent to explicit implementation except for overriding, which is legal in IL
private sealed override object Foo.M() {
return M_helper();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private string M_helper() {
return string.Concat(base.M(), "buzz");
}
public new virtual string M() {
return M_helper();
}
} Update: Realized that this doesn't make sense. If the overriding method called some non-virtual private method then calling the virtual method on the base class wouldn't result in a call to an overriding method of the virtual shadow method. 2.. This is true, and I don't know that there is anything that can be done to directly address that. I will be honest that part of the reason that I don't like the attribute direction is that I've been doing a lot of Java lately and the concept of type-erasure just makes my skin crawl. 😄 |
@sharwell Well you're definitely right about the base method overhead. Put together a test going three levels of overrides deep using the explicit overrides and shadowing strategy as well as standard overrides. I have three classes, cleverly named In the test I create an instance of
Here is the IL for these methods: Covariant Returns IL And here is the C# for the test project: Covariant Returns Test Project I don't doubt that this test could be done a little better and perhaps there are better strategies for supporting "true" covariant returns from the compiler. Despite the overhead I still prefer a method which results in a method signature that includes the covariant type. |
When implementing multiple levels of overriding of covariant-returning functions, it is no necessary for the compiler to generate a long chain of method invocations. The compiler can implement this with at most a single intermediate method at runtime. That is the way the Java compiler does it. |
If you're planning on shadowing, one extremely important gotcha to add to the list: IMO shadowing is dirtier from a conceptual standpoint and a reflection standpoint. |
@jnm2 Yes, you should expect the compiler to produce code that has the appropriate semantics. |
What about using the function to construct a delegate? This would work fine with the shadowing solution, but how would the reflection solution handle this case? |
Wow. Fantastic posts from @sharwell and @HaloFour. If you guys have any of your working samples around still, I'd love to see and try to understand a little bit better. In general, I see more value in the attribute approach as it would scale better with multiple levels if the inlining doesn't work out as guaranteed, but one thing that I wonder about is the casting issue. Would it work with a direct cast, or would it have to be a full conversion cast? |
Definitely second this feature - it's a feature we've been asking for years. Although one should favor composition over inheritance, this is a feature that one expects to be available in a modern version of the language. And no, the current workaround with shadowing the inherited member is not something the team should be proud of pushing to the developers. |
@Thaina I don't really understand your proposal. Either the class is generic or it isn't. What kind of CLR code is generated under the covers has nothing to do with how the language is specified. @iskiselev Yes, covariant returns should work for all kinds of implicit reference conversions (6.1.7). But adding support for contravariant method arguments would be a breaking change. |
@gafter The purpose of main proposal, aka covariant return type, is to be able to mark the type of virtual function to be derived class when it was overridden Most of the time, it is used to let virtual function return derived class So I just see that we could do it by using generic. Normally I would make a class like this
What I want to propose is, we should have
Or maybe it could just allow using
|
@Thaina This proposal is independent of that specific use case, although it does manage to satisfy the need specifically in overridden virtual methods. Note that modifying the arity/genericity of a class does have a real impact. |
Will this work with read-only properties also? |
@HaloFour I'm not sure what's more use case we should apply to. To let covariant return type can return anything other than derived class it then make a class couple dependencies The most agreeable use cases is the derived class like the example, other than that we should use generic to mutate type And about the same name problem, that's why I was propose
|
@Thaina A lack of imagination is no reason to hobble a feature, particularly by trying to tie it to generics which has absolutely nothing to do with this. I use this feature frequently in Java, where it is supported, and it is useful in allowing me to avoid extraneous casts, even in cases where I'm not trying to create a fluent API. RxJersey uses it to narrow the results from reactive invoker implementations to the appropriate result type, critical for use in its fluent API. And the feature doesn't require generics in Java either. |
@HaloFour I was using my imagination more than you to suspect that it really useful BUT it then make a class couple dependency It make dependencies between class like Suppose we support return covariant on anything
Did you see B then making chain dependency on LL? Which then it would more benefit to have base class specify And if there are any other things that would be able to change type in the future. I think that then it should be generic from the start Something like this
|
Yes, I do see B taking a dependency on LL, because B has a dependency on LL. Being able to do that is the entire purpose of this proposal. I can then assign the result of
vs:
Even more fun when you chain it a couple of levels. |
@HaloFour That is not entire purpose. Even the main example of proposal is about return the derived class, not arbitrary covariant And now you are the one who don't use imagination. Do you see my example?
Then when we use B it will be like this |
@Thaina No, that's one use case. I'd suggest reading the first paragraph of this proposal again. It clearly states:
Generics are completely unnecessary here and would create a SIGNIFICANTLY sloppier solution that CANNOT be extended to existing interfaces or classes. I'm sorry that you have problems understanding this. |
@HaloFour I know that is one use case so I was state most of the time I understand the situation and I could say You are underestimate generic. Surely it not necessary to but it could solve problem in another way if you use your imagination |
@Thaina In your example, There are some other problems with your example as well. The primary one involves the coupling argument. Suppose you have the following: class L { }
class LL : L { }
class A
{
virtual L GetL() { return new L(); }
}
class B : A
{
override L GetL() { return new LL(); }
} As you can see, I also notice that it appears that you are confusing this issue with #155. However, covariant return types are a completely different proposal from the special case described there. |
@sharwell For your first paragraph, that's why I was thinking about something like #155 so it would not need generic For your last argument. I'm not confuse. I'm just thinking we could do things another way around for problem of the main proposal. I don't think a couple design like that should be used And for covariant with things other than self. Should we use generic constraint instead of override? That's what I'm thinking about |
@iskiselev @gafter I have a unit test for that case, it will have to work as expected. Speaking of unit tests, I need some advice. Right now I have placed all my tests in a new file, but I am not sure this is the right way to go. I am thinking to add tests for the positive, usable, "should compile and work" cases here: And the corresponding tests on (compiled) attributes etc. in Then, place the little source snippet tests (both for positive, should compile cases and for "should give error" cases) under But, should I keep them in a separate file (CovariantOverrideTests.cs). or put them in an existing file, or split them among several existing files, e.g. ? |
@ldematte Placing all of the new tests in one or more new files is probably a good way to go. Negative tests (error messages) and positive tests (program behavior) are fine to be mixed if you want, or separated, whichever feels best to you. Some people prefer they be separated, so if you want to avoid review comments suggesting that, I guess separating them is preferred. It is a good idea to keep the metadata tests separate. |
Would it be possible to return a constrained generic type instead of a concrete type? class Foo {
public virtual object P1 { get; }
public virtual Base P2 { get; }
}
class Foo<T,U> : Foo where T : class where U : Base {
public override T P1 { get; }
public override U P2 { get; }
} |
Is there any information available on whether this feature will make it in a future version of C#? If yes, which version? |
@Chiel92 no, there is no information available about when this might make it into C# |
This is now tracked by dotnet/csharplang#49 with a draft spec at https://github.com/dotnet/csharplang/blob/master/proposals/covariant-returns.md |
Support covariant return types. Specifically, allow an overriding method to have a more derived reference type than the method it overrides. This would apply to methods and properties, and be supported in classes and interfaces. This is one possible alternative to a
this
type proposed in #311.This would be useful in the factory pattern. For example, in the Roslyn code base we would have
The implementation of this would be for the compiler to emit the overriding method as a "new" virtual method that hides the base class method, along with a bridge method that implements the base class method with a call to the derived class method.
The text was updated successfully, but these errors were encountered: