Skip to content

C#9 Covariant Return Types #649

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

Closed
jonpryor opened this issue May 19, 2020 · 8 comments
Closed

C#9 Covariant Return Types #649

jonpryor opened this issue May 19, 2020 · 8 comments
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)

Comments

@jonpryor
Copy link
Member

C#9 will reportedly support covariant return types, which in many ways mirrors Java covariant return types.

Covariant return types is a feature in which a subclass of a type can be specified in a method override in a derived type; consider Java:

public interface Appendable {
    Appendable append(char c);
}

public class StringBuilder implements Appendable {
    StringBuilder append(char c) {…}
}

Because StringBuilder ISA Appendable, StringBuilder can be used as the Appendable.append(char) return type instead of using Appendable.

Compare to the existing Xamarin.Android Java.Lang.StringBuilder.Append(char) binding, in which IAppendable must be returned, because C# lacks covariant return types.

This is a welcome development, and means that it would (presumably) be easier to deal with binding them; see also #216.

Unknown concern #1: what does this do with assembly versioning? If e.g. Mono.Android.dll v12 embraces covariant return types such that StringBuilder.Append() now returns StringBuilder instead of IAppendable, will that break existing code?

For most circumstances, @jonpryor can't see any issues with emitting covariant return types, mirroring what Java does. (Assuming that we can, based on the previously mentioned ABI compatibility question.)

Collection Interfaces

Once area where there will be problems is around collection interfaces. Consider Issue #647, in which generator currently wants to bind DownloadDrawablesAsync.doInBackground(), a method which returns a java.util.HashMap<String, Drawable>, as a System.Collections.Generic.IDictionary<string, Drawable>. This is "friendlier" to consume, as a C# consumed, but there is no way for IDictionary<TKey, TValue> to be a subclass of Java.Util.HashMap.

It is plausible that, since we already special-case collection interfaces, they will need to continue to be special-cased.

@jpobst jpobst changed the title C#9 Covariant Return Types? C#9 Covariant Return Types May 19, 2020
@jpobst jpobst added enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.) labels May 19, 2020
@lambdageek
Copy link
Member

Note that in .NET 5, the runtime has more restrictions that the C# spec.

Covariant return types are for classes only:

  1. Only class C : B not interface I2 : I1 - C is allowed to change the return type of some method of B; I2 is not allowed to change the return type of some method in I1.
  2. There's one restriction on return types - if B has a method Foo that returns SomeInterface, then C can't have a method SomeStruct Foo (), where SomeStruct is a valuetype that implements SomeInterface.

Not sure how this compares to the Java covariant return types, but it's something to watch out for, potentially. (Particularly the first restriction)

@jpobst
Copy link
Contributor

jpobst commented Feb 23, 2021

Additional restrictions I found that are allowed in Java but not in C#9:

  1. Arrays (object[] -> string[])
  2. Generics (List<object> -> List<string>)
  3. Classes implementing interface methods:
interface ICloneable {
  object Clone ();
}

class MyClass : ICloneable {
  public MyClass Clone () { ... }
}

@jonpryor
Copy link
Member Author

jonpryor commented Mar 5, 2021

Classes implementing interface methods.

This can be supported via explicit interface implementation:

interface ICloneable {
  object Clone();
}

class MyClass : ICloneable {
  object ICloneable.Clone() {return Clone(); }
  public virtual MyClass Clone() {return this;}
}

@jonpryor
Copy link
Member Author

jonpryor commented Mar 5, 2021

Arrays (object[] -> string[]`)

This worked for me?

class A {
    public virtual object[] M() {return null;}
}

class B : A {
    public override string[] M() {return null;}
}

@jonpryor
Copy link
Member Author

jonpryor commented Mar 5, 2021

Unknown concern #1: what does this do with assembly versioning? If e.g. Mono.Android.dll v12 embraces covariant return types such that StringBuilder.Append() now returns StringBuilder instead of IAppendable, will that break existing code?

Answer: No (?)!

At least with current .NET 5.0.103, given:

// Assembly: Base.dll
public class Base {
    public virtual Base M() {return this;}
}

// Assembly: Derived.dll - v1.0
public class Derived : Base {
    public override Base M() {return this;}
}

// Assembly: App.dll
public class App {
    static void Main(string[] args)
    {
        Derived d = new Derived ();
        d.M();
    }
}

This is normal C#8 code; no covariant return types. As expected, App.Main contains:

callvirt   instance [Base]Example.Base [Base]Example.Base::M()

If we change Derived.dll without rebuilding anything else:

// Assembly: Derived.dll - v2.0
public class Derived : Base {
    public override Derived M() {return this;}
}

Re-running dotnet path/to/App.dll results in no crash; it works.

From this we can infer that changing a method override to use covariant return types is an ABI-compatible change.

(At least for this simple scenario.)

I think this might even be a source-compatible change as well, though it will cause var to change its inferred type:

var d = new Derived();
var m = d.M();

m will be inferred as Base in a v1 world, and Derived in a v2 world. Offhand, I don't immediately see how this would cause currently compiling code to no longer compile, though this could change semantics:

class MaybeRelevant {
    public static void M(Base b) {}
    public static void M(Derived d) {}
}MaybeRelevant.M(new Derived().M());

A different MaybeRelefvant.M() overload would be called in a v1 vs. v2 world.

@jonpryor
Copy link
Member Author

jonpryor commented Mar 5, 2021

@jpobst: with this in mind, how crazy/impossible would it be to prototype covariant return type support in generator to see if we could release for .NET 6? If there were (minor!) breakage, .NET 6 is a fair time to consider it (depending on the breakage).

@jpobst
Copy link
Contributor

jpobst commented Mar 5, 2021

I am not sure what you mean by covariant return type support in generator. Today generator has no concept of covariant return types, and generates an API that matches the Java API.

Then when csc builds the generated code the user receives a compile error informing them that covariant return types are not supported in C#. The user must then use metadata to "fix" the covariant return type to match the base method.

Thus in a .NET6 world, generator will do the exact same thing it does today. However csc will happily compile the covariant return types instead of giving an error. Profit!

For example, if we wanted to convert Mono.Android.dll to contain covariant return types, no changes would be necessary in generator. Instead we would need to delete the metadata lines that "fix" the covariant return types.

@jpobst
Copy link
Contributor

jpobst commented Feb 13, 2023

Closing issue as this does not appear to require any support from JI.

@jpobst jpobst closed this as completed Feb 13, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Apr 13, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)
Projects
None yet
Development

No branches or pull requests

3 participants