Страницы

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

понедельник, 25 ноября 2019 г.

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


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


Ответы

Ответ 1



Для начала, общая причина, которая касается не только 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 мы бы вызывали этот метод на копии значения, и таким образом код бы не сработал.

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

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