Зачем нужен upcast (повышающее приведение типа)?

1,00
р.
Недавно обсуждалось, зачем нужен downcast — приведение типа от более общего к более конкретному. А нужен ли upcast (повышающее приведение) — явное приведение типов в обратную сторону, от более конкретного к более общему? Ведь мы ничего не теряем, работая с более конкретным объектом?

Ответ
Для начала, общая причина, которая касается не только C#, но и большинства объектно-ориентированных языков: семантика. Если у программиста есть объект конкретного типа, он тем не менее может хотеть работать с ним как с более общим объектом: programming against an interface, not implementation! Это позволяет убедиться, что в коде не используются лишние, конкретные свойства, что будет мешать в будущем обобщить код. Разумеется, обычно это слишком строгая цель, и без этого можно обойтись.
Следующая причина — выбор перегрузки, неполиморфного метода. В зависимости от статического типа объекта (при совпадающем динамическом типе) могут быть вызваны различные перегрузки при одинаково выглядящем коде. Примеры: Вызов нужной перегрузки: void f(object o) { Console.WriteLine("обрабатываем объект") }
void f(string s) { f((object)s) // избегаем рекурсии Console.WriteLine("дополнительная обработка для строки") }
string s = "Пушкин" f(s) // вызывает перегрузку со строкой f((object)s) // вызывает перегрузку с объектом
Ещё один пример, который часто встречается в коде: class X { public static bool operator == (X x1, X x2) { // оптимизация: проверим совпадение объектов if ((object)x1 == (object)x2) return true // далее более дорогая проверка равенства по свойствам } }
Вызов перекрытого метода: class Base { public void X() { Console.WriteLine("нужный метод") } }
class Derived : Base { public new void X() { Console.WriteLine("бесполезный метод") } }
Derived d = new Derived() ((Base)d).X()
Явная реализация интерфейса не позволяет вызвать метод по имени. class X : IDisposable { void IDisposable.Dispose() {} }
var x = new X() // ... ((IDisposable)x).Dispose() // по-другому не вызвать
В случаях, когда тип переменной выводится неявно из типа другой переменной, бывают случаи, когда нас не устраивает автоматически выведенный тип. Пример: var list = new[] { 1, 2 }.ToList() list.Add("ой")
Мы хотим получить список object'ов, но выведение типов даёт нам список int'ов. Мы можем написать var list = new[] { (object)1, 2 }.ToList() list.Add("ой")
так всё будет компилироваться. Ещё один тесно связанный случай — тернарный оператор. Если типы альтернатив различны, компилятор не может найти общий тип выражения, и приходится помогать: Animal animal = nya ? new Cat() : new Dog() // не компилируется в C# <9 Animal animal = nya ? (Animal)new Cat() : new Dog() // компилируется<br>(Очень похожая проблема возникает с Nullable-типами: int? result = good ? 1 : null требует явного преобразования одного из операндов-альтернатив.) Этот случай подсказал @Pavel Mayorov в комментариях, спасибо! Впрочем, как указывает @EvgeniyZ, в C# 9 с target typing в некоторых случаях преобразование типов не нужно: Animal animal = nya ? new Cat() : new Dog() // компилируется в C#9 var animal = nya ? new Cat() : new Dog() // не компилируется в C#9 var animal = nya ? (Animal)new Cat() : new Dog() // компилируется
Ещё одно применение — неявная упаковка (boxing). Например, функции типа GetEnumerator() могут вернуть объект типа-значения, который реализует интерфейс IEnumerator. Работать с ним не всегда удобно: static public IEnumerable MultiZip( this IEnumerable> sequences, Func, R> resultSelector) { var enumerators = sequences.Select(s => (IEnumerator)s.GetEnumerator()).ToList() try { while (enumerators.All(en => en.MoveNext())) yield return resultSelector(enumerators.Select(en => en.Current)) } finally { foreach (var en in enumerators) en.Dispose() } }
Если бы мы забыли upcast к IEnumerator, то в enumerators мог бы оказаться набор value type (и это так и есть в нашем случае!). При этом, поскольку мы мутируем наши энумераторы (MoveNext), то для случая value type мы бы вызывали этот метод на копии значения, и таким образом код бы не сработал.
Ещё один случай, наверное самый частый и самый незаметный — upcast происходит неявно и автоматически в том месте, когда мы присваиваем переменной базового типа выражение дочернего типа, или передаём в функцию, ожидающую базовый тип, аргумент производного типа. Например, такое бывает, когда мы пользуемся полиформными коллекциями (коллекция животных, содержащая и кошек, и собак).