2
public abstract class Base<T>
{
    protected abstract InnerFooBase<InnerBarBase<T>> GetInnerFoo();

    protected abstract class InnerFooBase<TFoo> where TFoo : InnerBarBase<T>
    {
    }

    protected abstract class InnerBarBase<TBar>
    {
    }
}

public class Implementation : Base<string>
{
    protected override InnerFooBase<InnerBarBase<string>> GetInnerFoo()
    {
        return new InnerFoo(); // Error
    }

    class InnerFoo : InnerFooBase<InnerBar>
    {
    }

    class InnerBar : InnerBarBase<string>
    {
    }
}

Fiddle

Line 18 gives the error "Cannot implicitly convert type Implementation.InnerFoo to Base<string>.InnerFooBase<Base<string>.InnerBarBase<string>>"

I can fix the error by having InnerFoo derive from InnerBarBase<string> rather than InnerBar, but I do not want to do so. I need the code to enforce the relationship beetween InnerFoo and InnerBar.

InnerFoo derives from InnerFooBase<InnerBar>, and InnerBar derives from InnerBarBase<string>. If GetInnerFoo is looking for InnerFooBase<InnerBarBase<string>>, why can't the types be converted to their bases automatically?

Brian Gradin
  • 2,165
  • 1
  • 21
  • 42

3 Answers3

8

To illustrate why this fails I am going to rename your classes as follows:

InnerBarBase<T> -> Cage<T>
string -> Fish
InnerBar -> GoldfishBowl
InnerFooBase -> Zoo
InnerFoo -> MiniAquarium

And I'm going to simplify your scenario to use assignment rather than virtual overriding. The problem is more easily seen with assignment.

All right. Let's set up the types.

class Animal {}
class Fish : Animal {}
class Cage<TAnimal> where TAnimal : Animal { }

A cage is a thing that can hold animals.

class GoldfishBowl : Cage<Fish> { }

A goldfish bowl is a cage that can hold fish.

class B<TAnimal> where TAnimal : Animal
{
    public class Zoo<TCage> where TCage : Cage<TAnimal>
    { 
        public void Add(TCage cage) {}
    }
}

A B<TAnimal>.Zoo<TCage> is a collection of cages that can hold the given kind of animal. Note that I have created an "Add" method not present in your original program.

class MiniAquarium : B<Fish>.Zoo<GoldfishBowl> { }

A miniature aquarium is a zoo that contains only goldfish bowls.

And now the question is: can I use a mini aquarium in a context where I want a zoo that contains fish cages? No! Here's why:

B<Fish>.Zoo<Cage<Fish>> zoo = new MiniAquarium();

Let's suppose that is legal. It is not legal. But let's suppose it is. What goes wrong? I create a second kind of fish cage:

class SharkTank : Cage<Fish> { }

And now it is perfectly legal for me to say:

zoo.Add(new SharkTank());

And now we have a mini aquarium -- which by definition contains only goldfish bowls -- and it has a shark tank in it.

The only place where the compiler can produce the error is on the conversion from MiniAquarium to zoo of fish cages.

You can only do this sort of conversion -- a covariant conversion -- if the types are interfaces, the type parameters are reference types, and the interface has no "add" style methods, and the varying parameter is marked "out".

Moving back into your domain: we cannot use an InnerFoo in a context where a InnerFooBase<InnerBarBase<string>> is needed. An InnerFoo is an InnerFooBase that consumes only InnerBars, but we need an InnerFooBase that can consume any InnerBarBase<string>. There might be derived classes of InnerBarBase<string> out there in the world that are not InnerBar. And so the type system refuses to let you do this.

Given how confusing this is, you might consider taking a step back and asking if your arrangement of generic types and nested types is simply too complex for future programmers to understand. There's no need to try to capture all possible restrictions on program behaviour in the type system. And as we've just seen, when you try to, sometimes the type system enforces restrictions you didn't intend to express.

Ermiya Eskandary
  • 15,323
  • 3
  • 31
  • 44
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
2

Take a look at Generic Classes.

If a generic class implements an interface, all instances of that class can be cast to that interface. Generic classes are invariant. In other words, if an input parameter specifies a List<BaseClass>, you will get a compile-time error if you try to provide a List<DerivedClass>.

In your example you are expecting the return type to be the base class InnerBarBase<string> but you are trying to return the derived class InnerFoo.

jegtugado
  • 5,081
  • 1
  • 12
  • 35
0

As @Thiago mentioned, it is a Covariance and Contravariance issue. But you can get around this by using "out" specification for the TFoo type parameter of InnerFooBase to tell the compiler that it is covariant.

i.e., change

protected abstract class InnerFooBase<TFoo> where TFoo : InnerBarBase<T>

to

protected interface IInnerFooBase<out TFoo> where TFoo : InnerBarBase<T> 

This question has got good explanation for using "out" specification: "out T" vs. "T" in Generics

Notice that InnerFooBase is no more an abstract class but an interface. That is a limitation of using "out". You might want to check if you can afford such a design change.

Community
  • 1
  • 1
amarnp
  • 11
  • 2