Недавно обсуждалось, зачем нужен 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 мы бы вызывали этот метод на копии значения, и таким образом код бы не сработал.
Комментариев нет:
Отправить комментарий