Почему С# из общих параметров типа нарушает ковариацию?

Непонятно, почему следующий фрагмент кода не ковариен?

  public interface IResourceColl<out T> : IEnumerable<T> where T : IResource {

    int Count { get; }

    T this[int index] { get; }

    bool TryGetValue( string SUID, out T obj ); // Error here?
    }

Ошибка 1 Неверная дисперсия: параметр типа T должен быть инвариантным Действителен в 'IResourceColl.TryGetValue(строка, вне T)'. 'T' ковариантны.

Мой интерфейс использует только параметр шаблона в выходных позициях. Я мог бы легко реорганизовать этот код на что-то вроде

  public interface IResourceColl<out T> : IEnumerable<T> where T : class, IResource {

    int Count { get; }

    T this[int index] { get; }

    T TryGetValue( string SUID ); // return null if not found
    }

но я пытаюсь понять, действительно ли мой исходный код нарушает ковариацию или это компилятор или ограничение .NET ковариации.

Ответ 1

Проблема действительно здесь:

bool TryGetValue( string SUID, out T obj ); // Error here?

Вы отметили obj как параметр out, который все равно означает, что вы проходите в obj, поэтому он не может быть ковариантным, поскольку вы оба передаете экземпляр типа T, а также возвращаете его.

Edit:

Эрик Липперт говорит, что лучше, чем кто-либо, я ссылаюсь на его ответ на "ref и out parameters in С# и не может быть помечен как вариант" и цитирует его в отношении out:

Должно ли быть законным делать T отмеченным как "вне" ? К сожалению нет. "вне" фактически не отличается от "ref" за кулисами. Единственный разница между "out" и "ref" заключается в том, что компилятор запрещает считывание из параметра out до того, как оно назначено вызываемым пользователем, и что компилятор требует назначения до возвращения вызываемого абонента как обычно. Тот, кто написал реализацию этого интерфейса в Язык .NET, отличный от С#, мог бы читать он был инициализирован, и поэтому он может использоваться как вход. Мы поэтому запретите маркировку T как "вне" в этом случае. Это прискорбно, но мы ничего не можем с этим поделать; мы должны соблюдать правила безопасности типа CLR.

Ответ 2

Здесь возможно обходное решение с использованием метода расширения. Не обязательно удобно с точки зрения разработчика, но пользователь должен быть счастлив:

public interface IExample<out T>
{
    T TryGetByName(string name, out bool success);
}

public static class HelperClass
{
    public static bool TryGetByName<T>(this IExample<T> @this, string name, out T child)
    {
        bool success;
        child = @this.TryGetByName(name, out success);
        return success;
    }
}

public interface IAnimal { };

public interface IFish : IAnimal { };

public class XavierTheFish : IFish { };

public class Aquarium : IExample<IFish>
{
    public IFish TryGetByName(string name, out bool success)
    {
        if (name == "Xavier")
        {
            success = true;
            return new XavierTheFish();
        }
        else
        {
            success = false;
            return null;
        }
    }
}

public static class Test
{
    public static void Main()
    {
        var aquarium = new Aquarium();
        IAnimal child;
        if (aquarium.TryGetByName("Xavier", out child))
        {
            Console.WriteLine(child);
        }
    }
}

Ответ 3

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

var someIResourceColl = new someIResourceCollClass<String>();
Object k;
someIResourceColl.TryGetValue("Foo", out k); // This will break because k is an Object, not a String

Ответ 4

Изучите этот маленький пример, и вы поймете, почему это не разрешено:

public void Test()
{
    string s = "Hello";
    Foo(out s);
}

public void Foo(out string s) //s is passed with "Hello" even if not usable
{
    s = "Bye";
}

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