C # generics — почему требуется явное приведение из конкретного типа обратно в T?

#c#

#c#

Вопрос:

Это беспокоило меня некоторое время. У меня возникли небольшие проблемы с пониманием того, почему явные приведения необходимы в следующем коде:

 public static class CastStrangeness
{
    public class A
    {
    }

    public class B
        : A
    {
    }

    public static void Foo<T>(T item)
        where T : A
    {
        // Works.
        A fromTypeParameter = item;

        // Does not compile without an explicit cast.
        T fromConcreteType = (T)fromTypeParameter;

        // Does not compile without an explicit cast.
        Foo<T>((T)fromTypeParameter);
    }

    public static void Bar(A item)
    {
        // Compiles.
        Foo<A>(item);
    }
}
  

Мне кажется, что T гарантированно будет A , поэтому, конечно, компилятор может сделать вывод, что любой экземпляр A гарантированно может быть присвоен T ? В противном случае я не смог бы передать A или B в Foo() . Итак, что я упускаю?

PS. Я пытался искать бесконечные перестановки ключевых слов в этом, но каждый результат, похоже, заканчивается ссылкой на ковариацию и контравариантность общих интерфейсов 🙂

Комментарии:

1. «конечно, компилятор мог бы сделать вывод, что любой экземпляр A гарантированно присваивается T» Почему вы так думаете?

2. Как кто-то упоминал ниже, T более специфичен, чем A (я раньше об этом не думал). Сейчас я чувствую себя немного глупо, но это небольшое изменение перспективы было тем, чего мне не хватало.

3. Кстати, фактический код, с которым я работал, был немного сложнее (и я думаю, что, возможно, этот пример кода неправильно демонстрирует проблему). Я создавал функцию обхода для b-дерева, где каждый узел имел тип BTreeNode, и у него были свойства слева и справа от типа BTreeNode. Функция обхода заключала эти BTreeNodes в кортеж<BTreeNode, …> и для этого требовалось приведение). Я не мог видеть ситуации, когда назначение завершилось бы неудачей, поэтому я решил, что неявное приведение должно работать.

Ответ №1:

Возьмем случай:

 Foo(new B());
  

Ваше первое назначение в порядке:

 A fromtypeParameter = item;
  

Начиная с B: A.

Но это назначение не подходит:

 T fromConcreteType = fromTypeParameter;
  

Потому что вы вполне могли бы назначить fromTypeParameter как:

 fromTypeParameter = new A();
  

Который вы, очевидно, не можете привести к T (который в данном случае равен B). T более специфичен, чем A, он может быть получен из A. Таким образом, вы можете пойти одним путем, но не другим, без явного приведения (которое может завершиться неудачей).

Комментарии:

1. «T более специфичен, чем A». Спасибо — это то, чего мне не хватало 🙂

Ответ №2:

Мне кажется, что T гарантированно будет A , поэтому, конечно, компилятор может сделать вывод, что любой экземпляр A гарантированно может быть присвоен T ?

Ну, нет.

string является объектом. Но

 string s = new object();
  

является незаконным.

Ответ №3:

Все T могут быть A … но не все A являются T.

Когда вы создаете тип, производный от A, и передаете его в общий метод, компилятор знает его как T , производный тип. Если вы не приведете его обратно, он не будет знать, что вам нужны A или T или любой тип в дереве наследования между T и A.

Эта логика применима независимо от того, используете вы дженерики или нет.

 public class A
{}

public class B : A
{}

public class C: B
{}

A animal = new C();
C cat = animal;   // wont compile as it does not know that A is a cat, 
                  // you have to cast even though it looks like 
                  // a cat from the new C();
  

Даже если у вас есть общее ограничение, это, как правило, относится к способу использования общего метода и предотвращает нарушение ограничения. Это не относится к какой-либо ссылке на производный тип через базовую ссылку. Хотя компилятор «мог» понять это, он может не знать, что это было вашим намерением, поэтому лучше, чтобы это было безопасно и не выводило его.

Комментарии:

1. Спасибо, да — теперь я вижу, что упускал очевидное (честно говоря, я просто потратил слишком много времени на размышления об этом и был слишком близок к проблеме).

Ответ №4:

Это менее сложно, если вы используете более конкретные имена типов и переменных:

 public class Animal
{}

public class Giraffe : Animal
{}

public static void Foo<TAnimal>(TAnimal animal) where TAnimal : Animal
{
    Animal generalAnimal = animal;

    TAnimal possiblyMoreSpecificAnimal = (TAnimal) generalAnimal;

    // The above line only works with a cast because the compiler doesn't know
    // based solely on the variable's type that generalAnimal is a more
    // specific animal.
}