Why isn't there generic variance for classes in C# 4.0?
Asked Answered
J

2

68

If we have it for interfaces, why dont we have it also for classes? What would be the problem that we would incur when using it?

Jardena answered 28/4, 2010 at 21:44 Comment(3)
Post some trivial example of what you're talking about. I can't think of a scenario where I would use this.Examen
@Ian P: see my answer for an example.Wayless
possible duplicate of Why does C# (4.0) not allow co- and contravariance in generic class types?Undergird
W
131

Suppose you had a class C<T> that was covariant in T. What might its implementation look like? T has to be out only. That means that C<T> cannot have any method that takes a T, any property of type T with a setter, or any field of type T, because fields are logically the same as property setters; T goes in.

Pretty much the only useful thing you could build with a covariant class is something immutable as far as T is concerned. Now, I think it would be awesome to have covariant immutable lists and stacks and whatnot that were class types. But that feature is not so obviously awesome that it would clearly justify the massive expenditure in making the type system natively support covariant immutable class types.

A comment above asked for an example of where this would be useful. Consider the following sketch:

sealed class Stack<out T>
{
    private readonly T head;
    private readonly Stack<T> tail;
    public T Peek() { return head; }
    public Stack<T> Pop() { return tail; }
    public Stack(T head, Stack<T> tail)
    {
        this.tail = tail;
        this.head = head;
    }
}
static class StackExtensions
{
    public static Stack<T> Push<T>(this Stack<T> tail, T head) 
    {
        return new Stack<T>(head, tail);
    }
    public static bool IsEmpty<T>(this Stack<T> stack)
    {
        return stack == null;
    }
}

Suppose you had covariant classes. Now you can say

Stack<string> strings = null;
strings = strings.Push("hello");
strings = strings.Push("goodbye");
Stack<object> objects = strings;
objects = objects.Push(123);

And hey, we just pushed an integer onto a stack of strings, but everything worked out just fine! There's no reason why this couldn't be typesafe. An operation which would violate type safety on a mutable data structure can be safely covariant on an immutable data structure.

Wayless answered 29/4, 2010 at 0:35 Comment(10)
The idea of immutable collections is so useful that Scala has a whole set of immutable collections, and they all support covariance.Walburga
Classes that were covariant with regard to type T would be useful in cases where every instance would receive in its constructor all of the T's or T-creators it would ever need; classes that were contravariant with regard to type T would be most useful in cases where the class would receive in its constructor one or more T-consumers. Such situations are not necessarily rare or arcane, but in most of them consumers of the class could use a covariant/contravariant interface type instead of the class type.Gunn
Why is Stack mark as sealed? Is it related to variance?Luxuriant
@mathk: Extensibility is a feature, and has costs. Designing for extensibility is hard; it requires a lot of research into how users will extend the class in order to make sure that the extensibility points are correct. Therefore always seal any class that you do not intend to be extended.Wayless
I don't really have the same opinion on that, if a user want to extend a class I rather let him do what he want. If it is a silly thing to do at some point he will realize that by himself. Knowing the good OO practice is more valuable then enforcing them.Luxuriant
@mathk: And do you make all your properties that could be read-only into read-write properties? If a user wants to write a property, let them do it! If that turns out to cause a bug, they'll figure it out when the brakes fail or the lander crashes into the surface of the planet, or whatever the consequence of the bug is. After all, knowing about accessibility is more important than enforcing it. Would you agree with that, or would you make properties that are intended to be read-only into read-only properties? How is that different?Wayless
Properties are a different matter. They are link to encapsulation. Subclassing is more about changing the behavior. I can see many way to extend the Stack in your example. Suppose you what a distributed Stack, a persistent stack... After all If you don't test your lander before you launch it you deserve the crash.Luxuriant
@mathk: Sure, and what guarantees that my base class will be useful to build a persistent stack, a distributed stack, etc? What guarantees that a change to the base class in the future will not break a derived class, a derived class that I do not know even exists? You are absolutely correct that subclassing is about changing behaviour; what you miss is that subclassing is also about relying upon services provided by a base class, services that the base class author might not have intended to provide to you.Wayless
@EricLippert Could you explain, why the class, covariant on T, cannot have readonly fields of type T? In your example these fields are ok and do not break type safety. These fields can be initialized in type constructor, which is always invariant on T and thus can accept values of type T.Hendren
@Ilya: Because the designers of the CLR type system did not create a type system in which classes that have readonly fields of T may be covariant in T. Would it be possible to create such a feature? Sure, it is logically sensible. It just wasn't ever implemented. Just because a feature is possible doesn't make it implemented; someone has to do the work.Wayless
H
4

The .NET team along with the C# and VB.NET team has limited resources, the work they have done on co- and contravariance solves most of the real world problem. Type systems are very complex to get right – a solution that works in 99.9999% of cases is not good enough if it leads to unsafe code in the other cases.

I don’t think the cost/time of supporting co- and contravariance specs (e.g. “in”/”out”) on class methods is of a great enough value. I can see very few cases when they would be useable – due to the lack of multiply class inheritance.

Would you rather had waited for another 6 months for .net so as to get this support?


Another way to think of this is that in .net

  • Interfaces / delegates – are used to model the conceptual type system of an application
  • Class are used to implement the above types
  • Class inheritance is used to reduce code duplication while doing the above
  • co- and contravariance is about the conceptual type system of an application
Histoid answered 29/9, 2010 at 10:16 Comment(1)
Great answer. Can you please elaborate on your last bulleted point: co- and contravariance is about the conceptual type system of an application? Why do you say it is about the conceptual? And so what if it is?Woundwort

© 2022 - 2024 — McMap. All rights reserved.