Почему С# не поддерживает варианты общих классов?

Возьмите этот небольшой пример LINQPad:

void Main()
{
    Foo<object> foo = new Foo<string>();
    Console.WriteLine(foo.Get());
}

class Foo<out T>
{
    public T Get()
    {
        return default(T);
    }
}

Не удается скомпилировать эту ошибку:

Недействительный модификатор отклонения. В качестве варианта можно указать только параметры интерфейса и типа делегата.

Я не вижу никакой логической проблемы с кодом. Все можно статически проверять. Почему это не разрешено? Может ли это вызвать некоторую несогласованность в языке или было ли это слишком дорогостоящим для реализации из-за ограничений в CLR? Если это последнее, что я должен знать разработчику об упомянутом ограничении?

Учитывая, что интерфейсы поддерживают его, я ожидал бы, что поддержка классов будет логически следовать из этого.

Ответы

Ответ 1

Одна из причин:

class Foo<out T>
{
  T _store;
  public T Get()
  {
    _store = default(T);
    return _store;
  }
}

Этот класс содержит функцию, которая не является ковариантной, поскольку она имеет поле, а поля могут быть установлены в значения. Это хотя и используется ковариантным способом, потому что ему присваивается значение по умолчанию только когда оно будет только null для любого случая, когда фактически используется ковариация.

Таким образом, неясно, допустим ли мы это. Не позволяя ему раздражать пользователей (это все равно соответствует тем же потенциальным правилам, которые вы предлагаете), но позволяет это сложно (анализ уже немного запутан, и мы даже не начинаем охотиться за действительно сложными делами).

С другой стороны, анализ этого гораздо проще:

void Main()
{
  IFoo<object> foo = new Foo<string>();
  Console.WriteLine(foo.Get());
}

interface IFoo<out T>
{
  T Get();
}

class Foo<T> : IFoo<T>
{
  T _store;
  public T Get()
  {
    _store = default(T);
    return _store;
  }
}

Легко определить, что ни одна из реализаций IFoo<T> не разрушает ковариацию, потому что у нее ее нет. Все, что необходимо, это убедиться, что в качестве параметра (в том числе метода setter) не используется T и.

Тот факт, что ограничение потенциала намного сложнее для класса, чем на интерфейсе по аналогичным причинам, также уменьшает степень полезности ковариантных классов. Они, конечно, не были бы бесполезны, но баланс того, насколько полезными они были бы в отношении того, сколько работы было бы, чтобы указать и реализовать правила о том, что им разрешено делать, намного меньше, чем баланс того, насколько полезны ковариантные интерфейсы о том, как много работы было указать и реализовать их.

Конечно, разницы достаточно, чтобы пройти мимо точки "хорошо, если вы собираетесь разрешить X, было бы глупо не допускать Y...".

Ответ 2

Класс должен содержать только параметры выходного метода (для ковариантности) и только параметры входного метода (для того, чтобы быть контравариантным). Дело в том, что трудно гарантировать, что для классов: например, ковариантный класс (по типу T type) не может иметь полей T, потому что вы можете писать в эти поля. Он отлично подойдет для действительно неизменяемых классов, но в настоящее время нет полной поддержки неизменности в С# (например, как в Scala).