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

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

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

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

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

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

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

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

2 ответа

Решение

Одной из причин будет:

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> нарушает ковариацию, потому что у нее нет. Все, что нужно, это убедиться, что нет никакой пользы от T в качестве параметра (в том числе метод сеттера), и это сделано.

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

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

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

Другие вопросы по тегам