46

What is wrong with this?

interface IRepository<out T> where T : IBusinessEntity
{
    IQueryable<T> GetAll();
    void Save(T t);
    void Delete(T t);
}

It says:

Invalid variance: The type parameter 'T' must be contravariantly valid on 'MyNamespace.IRepository.Delete(T)'. 'T' is covariant.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Eduardo
  • 5,645
  • 4
  • 49
  • 57
  • 1
    What did you end up doing? I'm facing the same problem. The answers don't really solve it; I need GetAll, Save and Delete in the same class – David Aug 07 '15 at 08:51
  • 1
    Sorry I don't remember. It was 4 years ago. – Eduardo Aug 07 '15 at 19:35

3 Answers3

74

Consider what would happen if the compiler allowed that:

interface IR<out T>
{
    void D(T t);
}

class C : IR<Mammal>
{
    public void D(Mammal m)
    {
        m.GrowHair();
    }
}
...
IR<Animal> x = new C(); 
// legal because T is covariant and Mammal is convertible to Animal
x.D(new Fish()); // legal because IR<Animal>.D takes an Animal

And you just tried to grow hair on a fish.

The "out" means "T is only used in output positions". You are using it in an input position.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 22
    I could never understand why when explaining something, how `T` and `IR` and `C` and `x` are valid variable names. This also applies to MSDN documentation, especially with generics. What's "D"? – David Aug 07 '15 at 08:54
  • 10
    @David: They are valid because they meet the criteria for identifiers in the C# specification, but I think you meant pedagogically valid. The pedagogy of using short names is to subtly remind the reader that this is a general, broadly applicable example that they should be thinking of in the abstract, and not a solution to a specific problem in a specific domain. – Eric Lippert Aug 19 '15 at 13:09
  • 2
    Not so pedagogically valid... when examples bring concept to mind while symbols bring significance that is far from conceptualization. But It's far from Variance. – Luc-Olivier Jul 09 '19 at 19:22
  • 6
    I wish I had seen this explanation when first trying to learn about co/contravariance: "The 'out' means 'T is only used in output positions'. You are using it in an input position." I had just assumed `in` and `out` were arbitrarily reused keywords. – Andrew Keeton Aug 21 '19 at 21:05
  • 3
    @AndrewKeeton: We went through a long design process where we considered a number of options. Like all design processes, there were many competing factors that had to be weighed against each other. See my 2007 article on the subject for some of the options and their pros and cons: https://blogs.msdn.microsoft.com/ericlippert/2007/10/31/covariance-and-contravariance-in-c-part-eight-syntax-options/ – Eric Lippert Aug 21 '19 at 21:09
  • @EricLippert: Why is `bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);` not considered covariant ? Most `IReadOnlyxxx` interfaces like `IReadOnlyList` are covariant, but `IReadOnlyDIctionary` is not, because of `TryGetValue`. Shouldn't returning a value be treated the same as an out parameter ? Isn't an out parameter in an 'output position' ? – Peter Huber Dec 23 '20 at 01:19
  • @PeterHuber: Great follow-up question. Already answered here: https://stackoverflow.com/questions/2876315/ref-and-out-parameters-in-c-sharp-and-cannot-be-marked-as-variant/2877515#2877515 -- when designing new APIs like `TryGetValue` today, the best practice is to make them return a `(bool, T)` tuple instead of using an out parameter, and then this problem disappears. – Eric Lippert Dec 23 '20 at 15:32
46

You can use an out type parameter only covariantly, i.e., in the return type. Therefore, IQueryable<T> GetAll() is correct, but void Delete(T t) is not.

Since T is used both co- and contravariantly in your class, you cannot use out here (nor in).

If you want to know more about the theoretical background behind this, take a quick break and read the "Covariance and Contravariance" Wikipedia article.


Welcome back. So, what do you do if you need all those methods in your repository but still need a covariant interface? You can extract the covariant part into its own interface:

interface IDataSource<out T> where T : IBusinessEntity
{
    IQueryable<T> GetAll();
}

interface IRepository<T> : IDataSource<T> where T : IBusinessEntity
{
    void Save(T t);
    void Delete(T t);
}

This is also how the .NET BCL solves this issue: IEnumerable<out T> is covariant, but only supports "read operations". ICollection<T> is a subtype of IEnumerable<out T>, allows read and write operations, and, thus, cannot be covariant itself.

Heinzi
  • 167,459
  • 57
  • 363
  • 519
  • 12
    Note that it *can* be in the parameters, but then only with something like an `Action` which reverses the direction again. – Jon Skeet Feb 18 '11 at 13:21
24

The following two methods are wrong:

void Save(T t);
void Delete(T t);

You can't have T as method argument. Only as return type if you want it to be covariant (out T) in your generic definition.

Or if you want contravariance then you could use the generic parameter only as method argument and not return type:

interface IRepository<in T> where T : IBusinessEntity
{
    void Save(T t);
    void Delete(T t);
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928