Search code examples
c#genericscovariancedowncast

c# Covariance Generics on Properties / Generic Access?


public interface IFoo { }
public abstract class IFooDerived : IFoo { }
public class Foo : IFooDerived { }

public interface IBar { }
public abstract class IBarDerived : IBar { }
public class Bar : IBarDerived { }

public interface IExample<out A, B> where A : IFoo where B : IBar
{
    A Foo(B b);
}

public class Example : IExample<Foo, Bar>
{
    static Example example = new Example();
    static Example()
    {
        Example ex = example;

        IExample<IFoo, IBar> ex_huh = ex; //How can I do this?

        //Or is there a way where I can say I don't care, but I still want to reference ex, like this:
        IExample<object,object> ex_huh2 = ex;
        //Or:
        IExample<, > ex_huh2 = ex;

        ex_huh2.Foo(new Bar()); //And still call Foo?
    }

    public Foo Foo(Bar b)
    {
        throw new NotImplementedException();
    }
}

In the above example, how do I downcast and reference the static example variable without knowing its generic types but still be able to call "A Foo(B b)"?

Is there a strongly typed way of doing this?


Solution

  • IExample<IFoo, IBar> ex_huh = ex; //How can I do this?
    

    You can't, because it would break type safety. And that's because you're trying to make your method's parameter type (that parameter to Foo(B b)) more general, and that's not safe.

    To make it easier to see why that's a problem, let's boil your example down to something equivalent (as far as that method parameter goes):

    function DoSomethingWithFoo(Foo foo) { ... }
    // The following line won't compile, but let's pretend it does and see what happens
    Action<IFoo> doSomethingWithIFoo = DoSomethingWithFoo;
    doSomethingWithIFoo(new OtherFoo());
    

    where OtherFoo is some other class that implements IFoo, but does not descend from the Foo class.

    Whoops! You're calling DoSomethingWithFoo, which expects an instance of Foo, but passing an OtherFoo instead! Wacky hijinks ensue.

    That's essentially what you're trying to do in your example. You're trying to take a class with a method that expects a parameter of the Foo type, and cast that to something that lets you pass any IFoo instead.

    (And that's why the compiler won't let you declare IExample's B type parameter as out B, which is what it would have to be to let you cast an IExample<..., Foo> to an IExample<..., IFoo>. The compiler sees that the B type parameter is used as the type of a method parameter, and that therefore, making it covariant would break type safety.)


    As for how to accomplish what you're looking for: that's going to depend. Your specific example of wanting to do

    IExample<...figure out how to declare this...> = ...any IExample<A, B>...;
    ex_huh2.Foo(new Bar());
    

    isn't going to work in general, because that "...any IExample<A, B>..." might well be an IExample<..., OtherBar>, and then you can't pass it a new Bar(). You'll have to figure out how you want to resolve that conflict.

    Maybe you really do want to pass in a new Bar(), in which case maybe you want to do that inside a method that's constrained to only taking IExamples with Bar as their second type parameter:

    public void AddABar<TFoo>(IExample<TFoo, Bar> example)
    {
        example.Foo(new Bar());
    }
    

    Or maybe you want to create a new instance of whatever the second type parameter is:

    public void AddAThing<TFoo, TBar>(IExample<TFoo, TBar> example)
        where TBar : new()
    {
        example.Foo(new TBar());
    }
    

    Or maybe you want to make IExample<A, B> descend from a non-generic IExample that declares a non-generic Foo(IBar b), implement that non-generic method on your Example class, and do a typecast to B inside that method -- taking the risk that if you pass the wrong type into that method at runtime, you'll get an InvalidCastException.

    It really all comes down to how you want to resolve that conflict of "I want to cast this to something I can pass a new Bar() to", when in point of fact any given implementation of IExample<A, B> won't necessarily be able to accept a new Bar() as a method parameter.