Що таке правильне використання каналу?


67

Даудаустинг означає передачу з базового класу (або інтерфейсу) на підклас або клас класу.

Прикладом пониження може бути, якщо ви переходите System.Objectна інший тип.

Downcasting непопулярний, можливо, кодовий запах. Об'єктно-орієнтована доктрина - це віддати перевагу, наприклад, визначенню та виклику віртуальних чи абстрактних методів замість того, щоб придушувати.

  • Які, якщо такі є, хороші та правильні випадки використання для приходу вниз? Тобто, за яких обставин (ив) доцільно писати код, який знищує?
  • Якщо ваша відповідь - "ні", то чому підтримка мовою підтримується мовою?

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. І amon в значній мірі дав вам відповідь - коли типова система, з якою ви працюєте, занадто обмежує, і ви не в змозі її змінити.
Звичайний

Відповіді:


12

Ось кілька належних способів використання каналу.

І я з повагою жорстоко не погоджуюся з іншими тут, які кажуть, що використання 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)
    }
}

Якщо ви збираєтеся сказати, що це кодові запахи, вам потрібно надати кращі рішення для них (навіть якщо їх уникнути через незручності). Я не вірю, що кращі рішення існують.


9
"Кодовий запах" не означає "ця конструкція, безумовно, погана ", це означає, що "ця конструкція є підозрілою ". Про те, що є мовні особливості, які за своєю суттю є підозрілими, - це питання прагматизму автора мови
Калет

5
@Caleth: І вся моя суть у тому, що ці конструкції виникають постійно, і що абсолютно нічого підозрілого щодо дизайнів, які я показав, немає абсолютно нічого , і, отже, кастинг не є кодовим запахом.
Мехрдад

4
@Caleth Я не згоден. Запах коду, для мене, означає, що код поганий або, принаймні, його можна покращити.
Конрад Рудольф

6
@Mehrdad Ваша думка, здається, полягає в тому, що запахи коду повинні бути виною програмних програмістів. Чому? Не кожен запах коду повинен бути актуальною проблемою або мати рішення. За словами Мартіна Фаулера, кодовий запах просто "поверхневий показник, який зазвичай відповідає глибшій проблемі в системі" object.Equals, цілком відповідає цьому опису (спробуйте правильно надати рівний контракт у суперкласі, який дозволяє підкласам правильно його реалізувати - це абсолютно нетривіальний). Те, що програмісти програм ми не можемо вирішити (в деяких випадках ми можемо цього уникнути) - це інша тема.
Во

5
@Mehrdad Що ж, давайте подивимось, що один із дизайнерів оригінальної мови має сказати про рівних .. Object.Equals is horrid. Equality computation in C# is completely messed up(коментар Еріка до його відповіді). Смішно, як ви не хочете переглянути свою думку навіть тоді, коли один із провідних дизайнерів мови сам каже вам, що ви неправі.
Voo

135

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

Я не погоджуюсь. Downcasting надзвичайно популярний ; величезна кількість реальних програм містять один або декілька знижень. І це не можливо кодовий запах. Це, безумовно, кодовий запах. Ось чому операція пониження моменту вимагає виявлення в тексті програми . Це так, що ви можете легше помітити запах і витратити на нього перегляд коду.

за яких обставин [и] доцільно писати код, який знищує?

За будь-яких обставин, коли:

  • у вас є 100% правильне знання факту про тип вираження, який є більш конкретним, ніж тип виразу часу компіляції, і
  • вам потрібно скористатися цим фактом, щоб використати можливості об'єкта, недоступного для типу компіляції, і
  • краще написати час та зусилля для написання ролі, ніж рефактор програми, щоб усунути будь-який із перших двох пунктів.

Якщо ви можете дешево переробити програму, щоб компілятор міг визначити або тип виконання, або переробити програму, щоб вам не потрібні можливості більш похідного типу, тоді зробіть це. Зниження мови було додано до мови для тих обставин, коли важко і дорого таким чином переробити програму.

чому підтримка мовлення підтримується мовою?

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

Крім того, посилання вниз # є дуже безпечним у C #. Ми маємо тверду гарантію, що вихідний режим буде перевірений під час виконання, і якщо його не вдасться перевірити, програма зробить правильно і вийде з ладу. Це чудово; це означає, що якщо ваше 100% правильне розуміння семантики типів виявляється правильним на 99,99%, то ваша програма працює 99,99% часу і решту часу припиняє роботу, замість того, щоб вести себе непередбачувано і пошкоджувати дані користувачів 0,01% часу .


Вправа: Існує щонайменше один спосіб створити пониження в C # без використання явного оператора лиття. Чи можете ви придумати будь-який подібний сценарій? Оскільки це також потенційні запахи коду, які дизайнерські фактори, на вашу думку, увійшли в розробку функції, яка могла б призвести до аварійного поломки, не маючи явного відтворення в коді?


101
Згадайте ті плакати в середній школі на стінах "Що популярне не завжди правильно, те, що правильно, не завжди популярне?" Ви думали, що вони намагаються утримати вас від вживання наркотиків або завагітніти у середній школі, але насправді вони попереджають про небезпеку технічної заборгованості.
corsiKa

14
@EricLippert, звичайно, заява foreach. Це було зроблено до того, як IEnumerable було загальним, правда?
Артуро Торес Санчес

18
Це пояснення зробило мій день. Як вчитель мене часто запитують, чому C # створений так чи інакше, і мої відповіді часто базуються на мотиваціях, якими ви та ваші колеги поділилися тут та у ваших блогах. Часто буває дещо складніше відповісти на наступне запитання "Але чому " чи так ?! ". І тут я знаходжу ідеальну подальшу відповідь: "C # був винайдений прагматичними програмістами, які мають працювати, для прагматичних програмістів, у яких є робота". :-) Дякую @EricLippert!
Зано

12
Убік: Очевидно, що в коментарі вище я мав на увазі сказати, що у нас є пониження з А до В. Мені дуже не подобається використання "вгору" і "вниз", "батьків" і "дитини" під час навігації по ієрархії класів, і я отримую їх весь час помилявся в моєму написанні. Зв'язок між типами є відношенням набір підмножини / підмножини, тому чому з нею повинен бути пов'язаний напрямок вгору або вниз, я не знаю. І навіть не запускайте мене з "батьків". Батько Жирафа - не ссавець, батьки жирафи - містер та місіс Жирафа.
Ерік Ліпперт

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який робив це всередині країни. Хоча це не завжди практично або можливо.


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і кожен реалізуючий підтип мав би відповідну реалізацію.

Але що робити, якщо ці класи з бібліотеки, яку ви не можете або не хочете змінювати? Тоді іншого варіанту немає.


Це виглядає як вдалий час для використання шаблону відвідувачів. Або dynamicключове слово, яке досягає того ж результату.
користувач2023861

3
@ user2023861 Ні, я думаю, що схема відвідувачів залежить від співпраці з підкласами пожертвування - тобто пожертвування потрібно оголосити конспектом void accept(IDonationVisitor visitor), який реалізують його підкласи шляхом виклику конкретних (наприклад, перевантажених) методів IDonationVisitor.
ChrisW

@ChrisW, ти маєш рацію з цього приводу. Без співпраці ви все одно можете зробити це за допомогою dynamicключового слова.
користувач2023861

У цьому конкретному випадку ви можете додати метод оцінювання Валюта до пожертви.
користувач253751

@ user2023861: dynamicвсе ще пригнічується , лише повільніше.
Мехрдад

12

Щоб додати відповідь Еріка Ліпперта, оскільки я не можу коментувати ...

Часто ви можете уникнути зриву каналів, перетворюючи API на використання генеричних даних. Але дженерики не були додані до мови до версії 2.0 . Тож навіть якщо ваш власний код не потребує підтримки старовинних мовних версій, ви можете опинитися за допомогою застарілих класів, які не параметризовані. Наприклад, якийсь клас може бути визначений для перенесення корисного навантаження типу Object, який ви змушені передати тому типу, яким ви насправді є.


Дійсно, можна сказати, що дженерики в Java або C # по суті є лише безпечною обгорткою навколо зберігання об'єктів надкласового класу та зведення каналу до правильного (перевіреного компілятором) підкласу перед тим, як використовувати елементи знову. (На відміну від шаблонів C ++, які перезаписують код, щоб завжди використовувати сам підклас і, таким чином, уникати необхідності знижувати - що може підвищити продуктивність пам’яті, якщо часто за рахунок часу компіляції та розміру виконуваного файлу.)
ліворуч від

4

Існує компроміс між статичними та динамічно набраними мовами. Статичний типізація дає компілятору багато інформації, щоб можна було зробити досить міцні гарантії щодо безпеки (частин) програм. Однак це досягає великих витрат, тому що ви не тільки повинні знати, що ваша програма правильна, але ви повинні написати відповідний код, щоб переконати компілятора, що це так. Іншими словами, пред'являти претензії простіше, ніж доводити їх.

Існують "небезпечні" конструкції, які допомагають зробити недоведені твердження компілятору. Наприклад, безумовні дзвінки до Nullable.Value, безумовних похибок, dynamicоб'єктів тощо. Вони дозволяють стверджувати претензію ("Я стверджую, що об'єкт a- це String, якщо я помиляюся, кидаю InvalidCastException"), не потребуючи доказу. Це може бути корисно у випадках, коли довести це значно важче, ніж варто.

Зловживання цим є ризикованим, саме тому явна нотація знищення існує і є обов'язковою; його синтаксична сіль покликана привернути увагу до небезпечної операції. Мова могла бути реалізована за допомогою неявного зменшення мовлення (де висновок типу однозначний), але це приховувало б цю небезпечну операцію, що не бажано.


Я думаю, я прошу, щоб, наприклад, де-небудь приклади того, де / коли потрібно або бажано (тобто належна практика) зробити подібне "недоведене твердження".
ChrisW

1
Як щодо виведення результату операції відображення? stackoverflow.com/a/3255716/3141234
Олександр

3

Пониження мов вважається поганим з кількох причин. В першу чергу я думаю, тому що це анти-OOP.

OOP дуже хотілося б, якби вам ніколи не доводилося зводити з ладу, оскільки його "raison-d'etre" полягає в тому, що поліморфізм означає, що вам не доведеться їхати

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

ти просто робиш

x.DoThing()

і код автоматично магічно робить все правильно.

Є деякі "важкі" причини, щоб не збивати з ладу:

  • У С ++ це повільно.
  • Якщо ви вибрали неправильний тип, ви отримаєте помилку виконання.

Але альтернатива збитковість в деяких сценаріях може бути досить потворною.

Класичний приклад - обробка повідомлень, де ви не хочете додавати функцію обробки до об’єкта, а зберігати її в процесорі повідомлень. Тоді я маю тонну MessageBaseClassмасиву для обробки, але мені також потрібна кожна підтип для правильної обробки.

Ви можете скористатися функцією Double Dispatch, щоб подолати проблему ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Але це також має свої проблеми. Або ви можете просто спуститися за якийсь простий код, ризикуючи з цих важких причин.

Але це було до того, як були винайдені дженерики. Тепер ви можете уникнути зриву, надавши тип, де деталі вказані пізніше.

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

Звичайно, ніхто не змушує вас писати код OOP, і є багато мовних функцій, які надаються, але нахмурилися, як-от рефлексія.


1
Я сумніваюся, що у C ++ це повільно, особливо якщо ви static_castзамість цього dynamic_cast.
ChrisW

"Обробка повідомлень" - я вважаю, що це означає, наприклад, підказки lParamна щось конкретне. Я не впевнений, що це хороший C # приклад, однак.
ChrisW

3
@ChrisW - ну так, static_castзавжди швидко ... але не гарантується, що не вийде з ладу невизначеними способами, якщо ви помилитесь і тип не такий, якого ви очікуєте.
Жуль

@Jules: Це зовсім поза точкою.
Мехрдад

@Mehrdad, саме в цьому справа. робити еквівалентний c # кидок у c ++ повільно. і це традиційна причина проти кастингу. альтернативний static_cast не еквівалентний і вважається гіршим вибором через невизначеність поведінки
Еван

0

Крім усього сказаного раніше, уявіть властивість Tag, яка є об'єктом типу. Він надає спосіб зберігати обраний вами об'єкт в іншому об'єкті для подальшого використання у міру необхідності. Вам потрібно скинути цю власність.

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


Так, див. Для прикладу. Для чого використовується властивість Tag у .net . В одній з відповідей там, хтось написав, я використовував це для введення інструкцій користувачеві в додатках Windows Forms. Коли спрацьовує подія управління GotFocus, властивості Label.Text властивості було призначено значення мого властивості тегу управління, яке містило рядок інструкцій. Я думаю, альтернативним способом "розширити" Control(замість використання object Tagвластивості) було б створити мету, Dictionary<Control, string>в якій зберігати мітку для кожного елемента управління.
ChrisW

1
Мені подобається ця відповідь, оскільки Tagце публічна властивість рамкового класу .Net, що показує, що ви (більш-менш) повинні використовувати його.
ChrisW

@ChrisW: Не сказати , висновок неправильно (або вправо), але врахуйте , що це неакуратно міркування, тому що є багато функціональних можливостей громадського .NET , який є застарілим (наприклад, System.Collections.ArrayList) або іншим чином засуджується (наприклад, IEnumerator.Reset).
Мехрдад

0

Найчастіше використання мовлення в моїй роботі пояснюється тим, що якийсь ідіот порушує принцип заміщення Ліскова.

Уявіть, що в якомусь сторонній lib є інтерфейс.

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();
    }
}

Ну ... зараз у мене немає вибору. Я маю набрати перевірку та (можливо) згорнуту роботу, щоб уникнути припинення моєї програми.


1
Неправда для цього конкретного прикладу. Ви можете просто зловити NotImplementedException, зателефонувавши на SayGoodbye.
Пер фон Цвайгберк

Можливо, це занадто спрощений приклад, але трохи попрацюйте зі старими 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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.