Что такое правильное использование downcasting?

68

Понижение означает преобразование из базового класса (или интерфейса) в подкласс или листовой класс.

Примером снижения может быть, если вы приведете System.Objectк другому типу.

Даункастинг непопулярен, может быть, пахнет кодом: доктрина объектно-ориентированного подхода предпочитает, например, определять и вызывать виртуальные или абстрактные методы, а не унижать.

  • Какие, если таковые имеются, хорошие и правильные варианты использования для даункинг? То есть, при каких обстоятельствах уместно писать код, который преуменьшает значение?
  • Если ваш ответ «нет», то почему языком поддерживается пониженная версия?
ChrisW
источник
5
То, что вы описываете, обычно называется «уныние». Вооружившись этим знанием, не могли бы вы сначала провести собственное исследование и объяснить, почему существующих причин, которые вы обнаружили, недостаточно? TL; DR: даункастинг нарушает статическую типизацию, но иногда система типов слишком ограничена. Так что для языка разумно предлагать безопасный даункинг.
Амон
Вы имеете в виду удручение? Прогнозирование будет происходить через иерархию классов, то есть от подкласса к суперклассу, в отличие от
понижения рейтинга
Мои извинения: вы правы. Я отредактировал вопрос, чтобы переименовать «upcast» в «downcast».
ChrisW
@amon en.wikipedia.org/wiki/Downcasting приводит в качестве примера слово «преобразовать в строку» (которое .Net поддерживает использование виртуального ToString); другой пример - «контейнеры Java», потому что Java не поддерживает обобщенные элементы (что делает C #). stackoverflow.com/questions/1524197/downcast-and-upcast говорит , что понижающее приведение есть , но без примеров , когда это уместно .
ChrisW
4
@ChrisW Это before Java had support for genericsне так because Java doesn't support generics. И Амон в значительной степени дал вам ответ - когда система типов, с которой вы работаете, слишком ограничена, и вы не можете ее изменить.
Ордос

Ответы:

12

Вот некоторые правильные способы использования downcasting.

И я с уважением категорически не согласен с другими здесь, которые говорят, что использование downcast - это определенно запах кода, потому что я считаю, что нет другого разумного способа решения соответствующих проблем.

Equals:

class A
{
    // Nothing here, but if you're a C++ programmer who dislikes object.Equals(object),
    // pretend it's not there and we have abstract bool A.Equals(A) instead.
}
class B : A
{
    private int x;
    public override bool Equals(object other)
    {
        var casted = other as B;  // cautious downcast (dynamic_cast in C++)
        return casted != null && this.x == casted.x;
    }
}

Clone:

class A : ICloneable
{
    // Again, if you dislike ICloneable, that's not the point.
    // Just ignore that and pretend this method returns type A instead of object.
    public virtual object Clone()
    {
        return this.MemberwiseClone();  // some sane default behavior, whatever
    }
}
class B : A
{
    private int[] x;
    public override object Clone()
    {
        var copy = (B)base.Clone();  // known downcast (static_cast in C++)
        copy.x = (int[])this.x.Clone();  // oh hey, another downcast!!
        return copy;
    }
}

Stream.EndRead/Write:

class MyStream : Stream
{
    private class AsyncResult : IAsyncResult
    {
        // ...
    }
    public override int EndRead(IAsyncResult other)
    {
        return Blah((AsyncResult)other);  // another downcast (likely ~static_cast)
    }
}

Если вы собираетесь сказать, что это запахи кода, вам нужно предоставить более эффективные решения для них (даже если их избежать из-за неудобств). Я не верю, что существуют лучшие решения.

Mehrdad
источник
9
«Кодовый запах» не означает «этот дизайн определенно плохой », это означает «этот дизайн подозрительный ». То, что есть языковые особенности, которые по своей природе являются подозрительными, является вопросом прагматизма со стороны автора языка
Caleth
5
@Caleth: И вся моя мысль в том, что эти проекты появляются постоянно, и что абсолютно нет ничего подозрительного в проектах, которые я показывал, и, следовательно, приведение не является запахом кода.
Mehrdad
4
@ Я не согласен. Для меня запах кода означает, что код плохой или, по крайней мере, может быть улучшен.
Конрад Рудольф
6
@ Mehrdad Ваше мнение таково, что запахи кода - это вина программистов приложений. Почему? Не каждый запах кода должен быть реальной проблемой или иметь решение. Запах кода в соответствии с Мартином Фаулером - это просто «поверхностный признак, который обычно соответствует более глубокой проблеме в системе», object.Equalsкоторый вполне соответствует этому описанию (попробуйте правильно предоставить контракт на равенство в суперклассе, который позволяет подклассам правильно его реализовать - это абсолютно нетривиальный). То, что как прикладные программисты мы не можем решить это (в некоторых случаях мы можем избежать этого), это отдельная тема.
Во
5
@Mehrdad Хорошо, давайте посмотрим, что один из дизайнеров оригинального языка скажет о Equals .. Object.Equals is horrid. Equality computation in C# is completely messed up(комментарий Эрика к его ответу). Забавно, что вы не хотите пересматривать свое мнение, даже если один из ведущих разработчиков языка говорит вам, что вы не правы.
Во
136

Даункастинг непопулярен, возможно, запах

Я не согласен. Даункинг чрезвычайно популярен ; огромное количество реальных программ содержат один или несколько downcast. И это не может быть запах кода. Это определенно кодовый запах. Вот почему операция даункейтинга должна быть указана в тексте программы . Это так, что вы можете легче заметить запах и уделить внимание проверке кода.

при каких обстоятельствах [s] уместно писать код, который преуменьшает?

В любых обстоятельствах, когда:

  • у вас есть 100% правильное знание факта о типе выражения во время выполнения, который является более конкретным, чем тип выражения во время компиляции, и
  • вам нужно воспользоваться этим фактом, чтобы использовать возможности объекта, недоступные для типа времени компиляции, и
  • это лучше использовать время и усилия, чтобы написать актерский состав, чем реорганизовать программу, чтобы исключить любой из первых двух пунктов.

Если вы можете дешево реорганизовать программу, чтобы компилятор мог определить тип среды выполнения или реорганизовать программу, чтобы вам не требовались возможности более производного типа, тогда сделайте это. Даункасты были добавлены к языку для тех обстоятельств, когда трудно и дорого рефакторинг программы.

почему даункинг поддерживается языком?

C # был изобретен прагматичными программистами, у которых есть работа, для прагматичных программистов, у которых есть работа. Дизайнеры C # не являются пуристами OO. И система типов C # не идеальна; по своей конструкции он недооценивает ограничения, которые могут быть наложены на тип переменной во время выполнения.

Кроме того, downcasting очень безопасен в C #. У нас есть надежная гарантия, что downcast будет проверен во время выполнения, и если его невозможно будет проверить, тогда программа сделает правильные вещи и вылетит. Это замечательно; это означает, что если ваше 100% правильное понимание семантики типов окажется правильным на 99,99%, то ваша программа работает 99,99% времени и в остальное время вылетает, а не ведет себя непредсказуемо и портит пользовательские данные 0,01% времени ,


УПРАЖНЕНИЕ: есть по крайней мере один способ произвести даункаст в C # без использования явного оператора приведения. Можете ли вы вспомнить какие-либо такие сценарии? Поскольку это также потенциальные запахи кода, как вы думаете, какие конструктивные факторы были заложены в разработку функции, которая могла бы привести к падению с понижением при отсутствии явного приведения в коде?

Эрик Липперт
источник
101
Помните те плакаты в старшей школе на стенах: «Что популярно, не всегда правильно, что правильно, не всегда популярно?» Вы думали, что они пытались удержать вас от наркотиков или забеременеть в старшей школе, но на самом деле они были предупреждением об опасности технического долга.
CorsiKa
14
@EricLippert, оператор foreach, конечно. Это было сделано до того, как IEnumerable был универсальным, верно?
Артуро Торрес Санчес
18
Это объяснение сделало мой день. Как учителя, меня часто спрашивают, почему C # спроектирован тем или иным образом, и мои ответы часто основаны на мотивах, которыми вы и ваши коллеги поделились здесь и в ваших блогах. Часто бывает сложнее ответить на последующий вопрос «Но почему ?». И здесь я нахожу идеальный последующий ответ: «C # был изобретен прагматичными программистами, у которых есть работа, для прагматичных программистов, у которых есть работа». :-) Спасибо, @EricLippert!
Зано
12
В сторону: Очевидно, что в моем комментарии выше я хотел сказать, что у нас есть пониженное значение от A до B. Мне очень не нравится использование «вверх», «вниз», «родитель» и «ребенок» при навигации по иерархии классов, и я получаю их неправильно все время в моем письме. Отношения между типами - это отношения надмножество / подмножество, поэтому, почему с этим должно быть направление вверх или вниз, я не знаю. И даже не заводите меня на "родителя". Родитель жирафа - не млекопитающее, родитель жирафа - мистер и миссис Жираф.
Эрик Липперт
9
Object.Equalsэто ужасно . Вычисление равенства в C # полностью испорчено , слишком запутано, чтобы объяснить это в комментарии. Есть так много способов представить равенство; очень легко сделать их непоследовательными; это очень легко, фактически требуется нарушить свойства, требуемые от оператора равенства (симметрия, рефлексивность и транзитивность). Если бы я сегодня проектировал систему типов с нуля, не было бы Equals(или GetHashcodeили ToString) Objectвообще.
Эрик Липперт
33

Обработчики событий обычно имеют подпись MethodName(object sender, EventArgs e). В некоторых случаях можно обрабатывать событие, не обращая внимания на то, что это за тип sender, или даже вообще не использовать sender, но в других senderнеобходимо привести к более конкретному типу для обработки события.

Например, у вас может быть два TextBoxs, и вы хотите использовать один делегат для обработки события от каждого из них. Затем вам нужно привести senderк TextBoxтак, чтобы вы могли получить доступ к необходимым свойствам для обработки события:

private void TextBox_TextChanged(object sender, EventArgs e)
{
   TextBox box = (TextBox)sender;
   box.BackColor = string.IsNullOrEmpty(box.Text) ? Color.Red : Color.White;
}

Да, в этом примере вы можете создать свой собственный подкласс, TextBoxкоторый делал это внутренне. Не всегда практично или возможно, хотя.

mmathis
источник
2
Спасибо, это хороший пример. TextChangedСобытие является членом Control- Интересно , почему senderэто не из типа Controlвместо типа object? Также мне интересно, могла ли (теоретически) структура переопределить событие для каждого подкласса (чтобы, например, TextBoxмогла иметь новую версию события, используя TextBoxв качестве типа отправителя) ... которая могла бы (или не могла) требовать понижения ( к TextBox) внутри TextBoxреализации (для реализации нового типа события), но не будет требовать понижения в обработчике события кода приложения.
ChrisW
3
Это считается приемлемым, поскольку трудно обобщить обработку событий, если каждое событие имеет собственную сигнатуру метода. Хотя я полагаю, что это приемлемое ограничение, а не то, что считается лучшей практикой, потому что альтернатива на самом деле более хлопотная. Если бы можно было начать с кода, TextBox senderа не object senderбез чрезмерного усложнения кода, я уверен, что это было бы сделано.
Нил
6
@Neil Системы обработки событий специально созданы для передачи произвольных блоков данных произвольным объектам. Это почти невозможно сделать без проверки времени выполнения на правильность типа.
Joker_vD
@Joker_vD В идеале API, конечно, будет поддерживать строго типизированные сигнатуры обратного вызова с использованием общей контравариантности. Тот факт, что .NET (в подавляющем большинстве случаев) не делает этого, объясняется просто тем, что дженерики и ковариация были введены позже. - Другими словами, снижение рейтинга здесь (и, как правило, в других местах) требуется из-за недостатков в языке / API, а не потому, что это в принципе хорошая идея.
Конрад Рудольф
@KonradRudolph Конечно, но как вы будете хранить события произвольных типов в очереди событий? Вы просто избавляетесь от очередей и отправляетесь на прямую отправку?
Joker_vD
17

Вам нужно понизить рейтинг, когда что-то дает вам супертип, и вам нужно обращаться с ним по-разному в зависимости от подтипа.

decimal value;
Donation d = user.getDonation();
if (d is CashDonation) {
     value = ((CashDonation)d).getValue();
}
if (d is ItemDonation) {
     value = itemValueEstimator.estimateValueOf(((ItemDonation)d).getItem());
}
System.out.println("Thank you for your generous donation of $" + value);

Нет, это не пахнет хорошо. В идеальном мире Donationбыл бы абстрактный метод, getValueи каждый реализующий подтип имел бы соответствующую реализацию.

Но что, если эти классы взяты из библиотеки, которую вы не можете или не хотите менять? Тогда другого варианта нет.

Philipp
источник
Это выглядит как хорошее время для использования шаблона посетителя. Или dynamicключевое слово, которое достигает того же результата.
user2023861
3
@ user2023861 Нет. Я думаю, что шаблон посетителя зависит от сотрудничества с подклассами Donation - т.е. Donation должен объявить реферат void accept(IDonationVisitor visitor), который его подклассы реализуют, вызывая определенные (например, перегруженные) методы IDonationVisitor.
ChrisW
@ChrisW, ты прав насчет этого. Без сотрудничества вы все равно можете сделать это, используя dynamicключевое слово.
user2023861
В этом конкретном случае вы можете добавить метод оценки стоимости в пожертвование.
user253751
@ user2023861: dynamicвсе еще удручает , только медленнее.
Мердад
12

Чтобы добавить к ответу Эрика Липперта, так как я не могу комментировать ...

Вы можете часто избегать снижения рейтинга путем рефакторинга API для использования обобщений. Но дженерики не были добавлены в язык до версии 2.0 . Поэтому, даже если ваш собственный код не нуждается в поддержке версий на древнем языке, вы можете использовать устаревшие классы, которые не параметризованы. Например, некоторый класс может быть определен так, чтобы нести полезную нагрузку типа Object, которую вы вынуждены привести к типу, который, как вы знаете, на самом деле является.

Нет U
источник
Действительно, можно сказать, что дженерики в Java или C # - это просто безопасная оболочка для хранения объектов суперкласса и перехода к правильному (проверенному компилятором) подклассу перед повторным использованием элементов. (В отличие от шаблонов C ++, которые переписывают код так, чтобы он всегда использовал сам подкласс и, таким образом,
исключал необходимость снижения
4

Существует компромисс между статическими и динамически типизированными языками. Статическая типизация дает компилятору много информации, чтобы иметь возможность давать довольно сильные гарантии безопасности (частей) программ. Однако это обходится дорого, потому что вам нужно не только знать, что ваша программа корректна, но вы должны написать соответствующий код, чтобы убедить компилятор, что это так. Другими словами, предъявлять претензии легче, чем доказывать их.

Существуют «небезопасные» конструкции, которые помогут вам сделать недоказанные утверждения для компилятора. Например, безусловные вызовы Nullable.Value, безусловные откаты, dynamicобъекты и т. Д. Они позволяют вам утверждать утверждение («Я утверждаю, что объект a- это String, если я ошибаюсь, выбрасываю InvalidCastException») без необходимости доказывать это. Это может быть полезно в тех случаях, когда доказать это значительно сложнее, чем стоит.

Злоупотребление этим рискованно, именно поэтому явная нисходящая нотация существует и является обязательной; это синтаксическая соль, предназначенная для привлечения внимания к небезопасной операции. Язык мог бы быть реализован с неявным понижением (где выводимый тип однозначен), но это скрыло бы эту небезопасную операцию, что нежелательно.

Александр
источник
Я полагаю, что я спрашиваю, например, о том, где / когда необходимо или желательно (например, хорошая практика) сделать такого рода «недоказанное утверждение».
ChrisW
1
Как насчет приведения результата операции отражения? stackoverflow.com/a/3255716/3141234
Александр
3

Даункинг считается плохим по нескольким причинам. В первую очередь я думаю, потому что это анти-ООП.

ООП действительно понравилось бы, если бы вам никогда не приходилось опускать руки, потому что его смысл - в том, что полиморфизм означает, что вам не нужно идти

if(object is X)
{
    //do the thing we do with X's
}
else
{
    //of the thing we do with Y's
}

ты просто делаешь

x.DoThing()

и код автоматически делает правильные вещи.

Есть несколько «веских» причин не опускать руки:

  • В C ++ это медленно.
  • Вы получаете ошибку во время выполнения, если выбираете неправильный тип.

Но альтернатива понижению в некоторых сценариях может быть довольно уродливой.

Классическим примером является обработка сообщений, когда вы не хотите добавлять функцию обработки к объекту, а оставляете ее в обработчике сообщений. Затем у меня есть тонна MessageBaseClassмассива для обработки, но для корректной обработки мне также нужен каждый из них по подтипу.

Вы можете использовать Double Dispatch, чтобы обойти проблему ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Но это также имеет свои проблемы. Или вы можете просто унынуть из-за простого кода, рискуя этими вескими причинами.

Но это было до того, как были изобретены дженерики. Теперь вы можете избежать даункинга, указав тип, где детали указаны позже.

MessageProccesor<T> может варьировать параметры метода и возвращаемые значения указанным типом, но все же предоставлять общие функциональные возможности

Конечно, никто не заставляет вас писать ООП-код, и существует множество языковых функций, которые предоставляются, но не одобряются, например рефлексия.

Ewan
источник
1
Я сомневаюсь, что это медленно в C ++, особенно если вы static_castвместо dynamic_cast.
ChrisW
«Обработка сообщений» - я думаю, что это означает, например, приведение lParamк чему-то конкретному Я не уверен, что это хороший пример C #, хотя.
ChrisW
3
@ChrisW - ну да, static_castвсегда быстро ... но не гарантируется, что он не потерпит неудачу неопределенными способами, если вы ошиблись, а тип не тот, который вы ожидаете.
Жюль
@Jules: Это совершенно не относится к делу.
Мехрдад
@ Mehrdad, это как раз то, что нужно. сделать эквивалентный C # приведение в C ++ медленно. и это традиционная причина против кастинга. альтернативный static_cast не эквивалентен и считается худшим выбором из-за неопределенного поведения
Ewan
0

В дополнение ко всему, что уже было сказано, представьте свойство Tag, имеющее тип object. Он позволяет вам сохранить выбранный вами объект в другом объекте для дальнейшего использования по мере необходимости. Вам нужно понизить это свойство.

В общем, неиспользование чего-либо до сегодняшнего дня не всегда является признаком чего-то бесполезного ;-)

шайба
источник
Да, см., Например, Как использовать свойство Tag в .net . В одном из ответов там, кто-то написал, я использовал его для ввода инструкций пользователю в приложениях Windows Forms. Когда сработало событие GotFocus элемента управления, свойству Label.Text для инструкций было присвоено значение моего свойства Tag элемента управления, содержащего строку инструкции. Я предполагаю, что альтернативный способ «расширить» Control(вместо использования object Tagсвойства) состоит в создании Dictionary<Control, string>метки для хранения метки для каждого элемента управления.
ChrisW
1
Мне нравится этот ответ, потому что Tagэто открытое свойство класса платформы .Net, которое показывает, что вы (более или менее) должны его использовать.
ChrisW
@ChrisW: Не сказать , вывод неправильно (или вправо), но учтите , что это неаккуратно рассуждения, потому что есть много функциональных возможностей общественного .NET , который является устаревшим (например, System.Collections.ArrayList) или иным образом осуждается (например, IEnumerator.Reset).
Мердад
0

Самое распространенное использование уныния в моей работе связано с тем, что какой-то идиот нарушает принцип замещения Лискова.

Представьте, что в какой-то третьей стороне есть интерфейс.

public interface Foo
{
     void SayHello();
     void SayGoodbye();
 }

И 2 класса, которые это реализуют. Barи Qux. Barхорошо себя ведет

public class Bar
{
    void SayHello()
    {
        Console.WriteLine(“Bar says hello.”);
    }
    void SayGoodbye()
    {
         Console.WriteLine(“Bar says goodbye”);
    }
}

Но Quxплохо себя ведет.

public class Qux
{
    void SayHello()
    {
        Console.WriteLine(“Qux says hello.”);
    }
    void SayGoodbye()
    {
        throw new NotImplementedException();
    }
}

Ну ... теперь у меня нет выбора. Я должен напечатать check и (возможно) downcast, чтобы избежать остановки моей программы.

Резиновая утка
источник
1
Не верно для этого конкретного примера. Вы можете просто перехватить NotImplementedException при вызове SayGoodbye.
Per von Zweigbergk
Возможно, это слишком упрощенный пример, но немного поработайте с некоторыми из старых API .Net, и вы столкнетесь с этой ситуацией. Иногда самый простой способ написания кода - безопасное приведение var qux = foo as Qux;.
RubberDuck
0

Другой реальный пример - WPF VisualTreeHelper. Он использует downcast для приведения DependencyObjectк Visualи Visual3D. Например, VisualTreeHelper.GetParent . Понижение происходит в VisualTreeUtils.AsVisualHelper :

private static bool AsVisualHelper(DependencyObject element, out Visual visual, out Visual3D visual3D)
{
    Visual elementAsVisual = element as Visual;

    if (elementAsVisual != null)
    {
        visual = elementAsVisual;
        visual3D = null;
        return true;
    }

    Visual3D elementAsVisual3D = element as Visual3D;

    if (elementAsVisual3D != null)
    {
        visual = null;
        visual3D = elementAsVisual3D;
        return true;
    }            

    visual = null;
    visual3D = null;
    return false;
}
zwcloud
источник