简体   繁体   中英

Constrained generics with hierarchy in type parameter

I have a problem with generics in C# I hope you can help me out with.

public interface IElement { }

public interface IProvider<T> where T : IElement {
    IEnumerable<T> Provide();
}

So far it's pretty simple. I want the providers to return enumerables of specific elements. A specific implementation of the interfaces is as follows:

public class MyElement : IElement { }

public class MyProvider : IProvider<MyElement> {
    public IEnumerable<MyElement> Provide() {
        [...]
    }
}

But the problem comes now when I want to use it. This does not compile because it cannot implicitly convert MyProvider into IProvider<IElement> :

IProvider<IElement> provider = new MyProvider();

I have to do a cast to IProvider<IElement> despite MyProvider is an IProvider<MyElement> and MyElement is an IElement . I could avoid the cast by making MyProvider also implement IProvider<MyElement> , but why does it not resolve the hierarchy in the type parameter?

EDIT: As per Thomas's suggestion, we can make it covariant in T . But what if there are other methods like below where there are arguments of type T ?

public interface IProvider<T> where T : IElement {
    IEnumerable<T> Provide();
    void Add(T t);
}

I have to do a cast to IProvider<IElement> despite MyProvider is an IProvider<MyElement> and MyElement is an IElement . Why does it not resolve the hierarchy in the type parameter?

This is a very frequently asked question. Consider the following equivalent problem:

interface IAnimal {}
class Tiger : IAnimal {}
class Giraffe : IAnimal {}
class MyList : IList<Giraffe> { ... }
...
IList<IAnimal> m = new MyList();

Now your question is: "I have to do a cast to IList<IAnimal> despite the fact that MyList is an IList<Giraffe> and Giraffe is an IAnimal . Why does this not work?"

It does not work because... suppose it did work:

m.Add(new Tiger());

m is a list of animals. You can add a tiger to a list of animals. But m is really a MyList, and a MyList can only contain giraffes! If we allowed this then you could add a tiger into a list of giraffes .

This must fail because IList<T> has an Add method that takes a T. Now, maybe your interface has no methods that takes a T. In that case, you can mark the interface as covariant , and the compiler will verify that the interface is truly safe for variance and allow the variance you want.

Since T only appears in output position in your IProvider<T> interface, you can make it covariant in T :

public interface IProvider<out T> where T : IElement {
    IEnumerable<T> Provide();
}

This will make this instruction legal:

IProvider<IElement> provider = new MyProvider();

This feature requires C# 4. Read Covariance and Contravariance in Generics for more details.

If you only use the reference to IProvider<IElement> to access methods that have T in an output position, you could segregate the interface into two (please find better names for them, like ISink<in T> for the contravariant one):

public interface IProviderOut<out T> where T : IElement {
  IEnumerable<T> Provide();
}
public interface IProviderIn<in T> where T : IElement {
  void Add(T t);
}

Your class implements both:

public class MyProvider : IProviderOut<MyElement>, IProviderIn<MyElement> {
  public IEnumerable<MyElement> Provide() {
    ...
  }
  public void Add(MyElement t) {
    ...
  }
}

But now you use the covariant interface when you need to upcast:

IProviderOut<IElement> provider = new MyProvider();

Alternatively, your interface can inherit from both:

public interface IProvider<T> : IProviderIn<T>, IProviderOut<T> 
  where T : IElement { 
  // you can add invariant methods here...
}

And your class implements it:

public class MyProvider : IProvider<MyElement> ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM