Advance usage of C# generic contravariants and covariants

62 Views Asked by At

In Kotlin, you can have this with generics:

class Foo<T> {
    fun toto(arg: Bar<in T>) {}

    fun tata(arg: Baz<out T>) {}
}

What is the equivalent in C#?

I tried to replicate the code in C# like this

class Foo<T> {
    public void toto(Bar<in T> arg) {}

    public void tata(Baz<out T> arg) {}
}

I expected for it to work, but it says Type argument is missing.

2

There are 2 best solutions below

2
Sweeper On

C# does not have use-site variance - it only allows you to specify the variance at the declaration (which you can also do in Kotlin), and only for interface types. e.g.

// this just so happens to also be valid Kotlin :)
interface IBar<in T> { ... }
// or
interface IBar<out T> { ... }

One way to "simulate" use-site variance is to divide up your class into "things that you can use covariantly" (T in an output position) and "things that you can use contravariantly" (T in an input position). Declare an interface for each of those two groups, with the correct variance.

interface ICovariantBar<out T> {
    T Output();
}

interface IContravariantBar<in T> {
    void Input(T t);
}

class Bar<T>: ICovariantBar<T>, IContravariantBar<T> {
    public void Input(T t) { ... }
    
    public T Output() { ... }
}

Then you can use ICovariantBar<T> for Bar<out T> in Kotlin, IContravariantBar<T> for Bar<in T> in Kotlin. For example, toto would be declared like this:

public void toto(IContravariantBar<T> arg) {}

This of course has a few caveats. From the top of my head:

  • the number of interfaces you would need grows combinatorially as you add more type parameters
  • any type can implement the interfaces, not just Bar<T>
  • C#'s variance doesn't work with value types like int. In Kotlin you can assign an instance of Bar<Int> to a Bar<out Any>, but Bar<int> is not convertible to a IConvariantBar<object>.
  • In Kotlin, you can technically still use methods with T in an input position when T is covariant, and methods with T in an output position when T is contravariant. It's just that the type T would be replaced by Nothing (the "bottom type") or the bound of T respectively. You'd need to add some extra methods in order to do the same in C#
0
Dai On

I'm unsure what exactly you're trying to accomplish, but this C# below shows (what I imagine) are similar use-cases:


class Biped
{
}

class Person : Biped
{   
}

class Superman : Person
{
}

void Main()
{
    Foo<Person> foo = new Foo<Person>();
    
    IContravariant<Biped> bipeds = new ContravariantImpl<Biped>();
    List<Superman> supermen = new List<Superman>();
    List<Person> normalmen = new List<Person>();
    
    foo.AcceptsACovariantInterfaceParameter( supermen );
    
    foo.AcceptsACovariantInterfaceParameter( normalmen );
    
    foo.AcceptAnInvariantClassParameter( supermen );
    
    foo.AcceptAContravariantInterfaceParameter( bipeds );
}



//

class Foo<T>
{
    public void AcceptsACovariantInterfaceParameter( IReadOnlyList<T> list ) // IReadOnlyList<T>, IReadOnlyCollection<T>, and IEnumerable<T> are covariant on T:
    {
        
    }
    
    public void AcceptAnInvariantClassParameter<T2>( List<T2> list ) // ... but the `T2 : T` constraint makes it feel like a variant generic method.
        where T2 : T
    {
    }
    
    public void AcceptAContravariantInterfaceParameter( IContravariant<T> foo )
    {
    }
}

//

interface IContravariant<in T>
{
    void Receive( T item );
}

class ContravariantImpl<T> : IContravariant<T>
{
    public void Receive( T item )
    {
        
    }
}