Страницы

Поиск по вопросам

вторник, 2 октября 2018 г.

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

Недавно обсуждалось, зачем нужен 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(); // не компилируется Animal animal = nya ? (Animal)new Cat() : new Dog(); // компилируется
(Очень похожая проблема возникает с Nullable-типами: int? result = good ? 1 : null требует явного преобразования одного из операндов-альтернатив.)
Этот случай подсказал @Pavel Mayorov в комментариях, спасибо! Ещё одно применение — неявная упаковка (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 мы бы вызывали этот метод на копии значения, и таким образом код бы не сработал.

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

Отправить комментарий