Чи є "Parent x = new Child ();" замість "Child x = new Child ();" поганою практикою, якщо ми можемо використовувати останнє?


32

Наприклад, я бачив деякі коди, які створюють такий фрагмент:

Fragment myFragment=new MyFragment();

який оголошує змінну як фрагмент замість MyFragment, який MyFragment є дочірнім класом Fragment. Я не задоволений цим рядком кодів, тому що я думаю, що цей код повинен бути:

MyFragment myFragment=new MyFragment();

Що конкретніше, це правда?

Або в узагальненні питання, чи погана практика використовувати:

Parent x=new Child();

замість

Child x=new Child();

якщо ми можемо змінити перший на другий без помилки компіляції?


6
Якщо ми зробимо останнє, то вже немає сенсу абстрагуванню / узагальненню. Звичайно, є винятки ...
Вальфрат

2
У проекті, над яким я працюю, мої функції очікують батьківського типу та запускають методи батьківського типу. Я можу прийняти будь-якого з кількох дітей, і код за тими (успадкованими та переосмисленими) методами відрізняється, але я хочу запустити цей код незалежно від того, якою дитиною я користуюся.
Stephen S

4
@Walfrat Дійсно, в абстракції немає сенсу, коли ви вже встановлюєте конкретний тип, але ви все одно можете повернути абстрактний тип, щоб код клієнта був блаженно невідомим.
Якоб Райхле

4
Не використовуйте батьків / дітей. Використовуйте інтерфейс / клас (для Java).
Thorbjørn Ravn Andersen

4
"Програмування на інтерфейс" від Google, щоб отримати пояснення щодо того, чому користуватися, Parent x = new Child()є хорошою ідеєю.
Кевін Уордман

Відповіді:


69

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

Прикладом може бути , що має , LinkedListі ArrayListякі обидва походять від List. Якщо код буде однаково добре працювати з будь-яким типом списку, то немає жодної причини довільно обмежувати його одним із підкласів.


24
Саме так. Щоб додати приклад, подумайте List list = new ArrayList(): Код, що використовує список, не має значення, який це список, лише той, що відповідає договору інтерфейсу List. Надалі ми можемо легко перейти на іншу реалізацію списку, і базовий код взагалі не потребуватиме змін або оновлення.
Можливо, фактор

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

10
@Mike: Я сподіваюся, що це само собою зрозуміло, інакше всі змінні, параметри та поля будуть оголошені як Object!
ЖакБ

3
@JacquesB: оскільки на це запитання та відповіді було приділено стільки уваги, я думав, що явна корисність буде корисною людям з різними рівнями кваліфікації.
Майк

1
Це залежить від контексту, це не відповідь.
Біллал Бегерадж

11

Відповідь ЖакБ є правильною, хоча і трохи абстрактною. Дозвольте більш чітко підкреслити деякі аспекти:

У своєму фрагменті коду ви явно використовуєте newключове слово, і, можливо, ви хочете продовжити подальші кроки ініціалізації, які, ймовірно, є специфічними для конкретного типу: тоді вам потрібно оголосити змінну з певним конкретним типом.

Інша ситуація, коли ви отримуєте цей предмет з іншого місця, наприклад IFragment fragment = SomeFactory.GiveMeFragments(...);. Тут завод зазвичай повинен повертати максимально абстрактний тип, і низхідний код не повинен залежати від деталей реалізації.


18
"хоч трохи абстрактно". Бадумтіш.
rosuav

1
хіба це не могло бути редагуванням?
beppe9000

6

Можливо, існують відмінності в продуктивності.

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

Приклад: ToStringце віртуальний дзвінок, але Stringзапечатаний. Увімкнено String, ToStringє неоперативним, тому якщо ви заявляєте, objectщо це віртуальний виклик, якщо ви оголошуєте Stringкомпілятор, він не знає, що жоден похідний клас не замінив метод, тому що клас запечатаний, тож це не-оп. Аналогічні міркування стосуються і ArrayListпроти LinkedList.

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


4
У більшості випадків відмінності в продуктивності є незначними. І в більшості випадків (я чув, що це говорило, що це приблизно 90% часу), ми повинні забути про ті невеликі відмінності. Тільки коли ми знаємо (бажано за допомогою вимірювання), що ми старіємо як критичний розділ коду, ми повинні дбати про такі оптимізації.
Жуль

І компілятор знає, що "нова дитина ()" повертає дитину, навіть коли вона призначена Батькові, і доки вона знає, він може використовувати ці знання та викликати код Child ()
gnasher729

+1. Також вкрав ваш приклад у моїй відповіді. = P
Nat

1
Зауважте, що існують оптимізації продуктивності, які змушують весь процес суперечити. Наприклад, HotSpot помітить, що параметр, переданий функції, завжди є одного конкретного класу, і відповідно оптимізує його. Якщо це припущення колись виявиться помилковим, метод деоптимізується. Але це сильно залежить від середовища компілятора / часу виконання. Востаннє я перевірив, що CLR не міг навіть зробити досить тривіальну оптимізацію, помітивши, що р з Parent p = new Child()- це завжди дитина ..
Voo

4

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

Припустимо, у вас є кілька тварин - Cat Birdі Dog. Тепер, ці тварини мають кілька загальної поведінки - move(), eat()і speak(). Всі вони харчуються по-різному і розмовляють по-різному, але якщо мені потрібно, щоб моя тварина їла чи говорила, мені не дуже важливо, як вони це роблять.

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

Тому в коді, який вимагає зловмисника, мені дійсно потрібно робити Dog fido = getDog(); fido.barkMenancingly();

Але більшу частину часу я можу із задоволенням змусити будь-яких тварин робити трюки, де вони бавляться, мяують чи твітнуть на частування. Animal pet = getAnimal(); pet.speak();

Отже, якщо бути конкретнішим, ArrayList має деякі методи, які Listцього не роблять. Слід зазначити, що trimToSize(). Зараз у 99% випадків нас це не хвилює. Це Listмайже ніколи не є достатньо великим для нас, щоб піклуватися. Але бувають випадки, коли це так. Тому періодично ми повинні спеціально просити ArrayListвиконати trimToSize()та зменшити його резервний масив.

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


Плутати, чому це може отримати зворотній зв'язок. Це здається канонічним, особисто. За винятком ідеї програмованого собаки-охоронця ...
corsiKa

Чому мені довелося читати сторінки, щоб дійти до найкращої відповіді? Рейтинг відстійний.
Василевс

Дякую за добрі слова. Існують плюси і мінуси в рейтингових системах, і один з них полягає в тому, що пізніші відповіді іноді можуть залишитись у пилу. Буває!
corsiKa

2

Взагалі кажучи, ви повинні використовувати

var x = new Child(); // C#

або

auto x = new Child(); // C++

для локальних змінних, якщо немає вагомих причин, щоб змінна мала інший тип, як те, з чим вона ініціалізована.

(Тут я ігнорую той факт, що в коді C ++, швидше за все, використовується інтелектуальний вказівник. Є випадки, коли не знаходяться більше, ніж один рядок, я фактично не можу знати.)

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


1
У C ++, змінні в основному створені без нового ключового слова: stackoverflow.com/questions/6500313 / ...
Маріан Spanik

1
Питання стосується не C # / C ++, а загального об'єктно-орієнтованого питання мовою, яка має хоч якусь форму статичного набору тексту (тобто, було б безглуздо запитати щось на зразок Ruby). Крім того, навіть у цих двох мовах я не бачу вагомих причин, чому ми повинні робити це так, як ви говорите.
AnoE

1

Добре тим, що це легко зрозуміти і легко змінити цей код:

var x = new Child(); 
x.DoSomething();

Добре, оскільки він повідомляє про наміри:

Parent x = new Child(); 
x.DoSomething(); 

Гаразд, тому що це звичайно і легко зрозуміти:

Child x = new Child(); 
x.DoSomething(); 

Єдиний варіант, який є по-справжньому поганим - використовувати, Parentякщо тільки Childє метод DoSomething(). Це погано, оскільки він повідомляє про наміри неправильно:

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

Тепер давайте детальніше зупинимось на тій справі, про яку конкретно задається питання:

Parent x = new Child(); 
x.DoSomething(); 

Форматуємо це інакше, зробивши другу половину викликом функції:

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

Це можна скоротити до:

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

Тут я припускаю, що прийнято вважати, що це WhichTypeповинен бути самий базовий / абстрактний тип, який дозволяє функціонувати роботі (якщо немає проблем з продуктивністю). У цьому випадку належного типу буде або є Parent, або щось Parentпоходить від цього.

Цей рядок міркувань пояснює, чому використання типу Parentє хорошим вибором, але він не входить у погоду, інші варіанти погані (вони не є).


1

tl; dr - використанняChildбільшParentпереважно в локальному масштабі. Це не тільки допомагає з легкістю для читання, але також необхідно забезпечити, щоб роздільна здатність перевантажених функцій працювала належним чином і сприяла ефективної компіляції.


На місцевому рівні,

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

Концептуально йдеться про підтримку максимально можливої ​​інформації. Якщо ми переходимо до рівня Parent, ми фактично просто викреслюємо інформацію про тип, яка могла б бути корисною.

Збереження повної інформації про тип має чотири основні переваги:

  1. Надає додаткову інформацію компілятору.
  2. Надає більше інформації для читача.
  3. Чистіший, більш стандартизований код.
  4. Робить логіку програми більш змінною.

Перевага 1: Більше інформації для компілятора

Очевидний тип використовується в роздільній роздільній здатності та оптимізації.

Приклад: Перевантажена роздільна здатність методу

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

У наведеному вище прикладі обидва foo()дзвінки працюють , але в одному випадку ми отримуємо кращу перевантажену роздільну здатність методу.

Приклад: Оптимізація компілятора

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

У наведеному вище прикладі обидва .Foo()виклики в кінцевому рахунку викликають один і той же overrideметод, який повертається 2. Просто в першому випадку існує пошук віртуального методу, щоб знайти правильний метод; пошук цього віртуального методу не потрібен у другому випадку з моменту цього методу sealed.

Подяка @Ben, який надав подібний приклад у своїй відповіді .

Перевага 2: Більше інформації для читача

Знаючи точний тип, тобто Childнадає більше інформації тому, хто читає код, полегшуючи зрозуміти, що робить програма.

Звичайно, може бути , це не має значення для фактичного коду , оскільки обидва parent.Foo();і child.Foo();мають сенс, але для кого - то , бачачи код в перший раз, більше інформації це просто корисна.

Крім того, залежно від середовища розробки IDE може надати корисніші підказки та метадані про, Childніж Parent.

Перевага 3: Чистіше, більш стандартизований код

Більшість прикладів коду C #, які я бачив останнім часом var, і це, в основному, скорочення Child.

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

Побачення varзаяви про недекларацію просто виглядає; якщо для цього є ситуаційна причина, приголомшливий, але в іншому випадку це виглядає анти-зразком.

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

Перевага 4: Більш змінна логіка програми для прототипування

Перші три переваги були великими; це набагато ситуативніше.

Тим не менш, для людей, які використовують код, як і інші, використовують Excel, ми постійно змінюємо наш код. Може бути , нам не потрібно викликати метод , унікальний для Childв цій версії коду, але ми могли б перепрофілювати або переробити код пізніше.

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

Підсумок

Використання Parentмес до перевантаженої роздільної здатності методу, гальмує деякі оптимізації компілятора, видаляє інформацію з зчитувача і робить код більш неприємним.

Використання var- це справді шлях. Це швидко, чисто, з малюнком та допомагає компілятору та IDE правильно виконувати свою роботу.


Важливо : ця відповідь відповідаєParentпорівняноChildз локальною сферою застосування методу. ПроблемаParentпорівняно зChildрізними для типів повернення, аргументів та класових полів.


2
Я додам, що Parent list = new Child()це не має великого сенсу. Код, який безпосередньо створює конкретний екземпляр об'єкта (на відміну від опосередкування через фабрики, фабричні методи тощо), має досконалу інформацію про тип, що створюється, можливо, знадобиться взаємодіяти з конкретним інтерфейсом для налаштування новоствореного об'єкта тощо. Місцевий обсяг - це не те, де ви досягаєте гнучкості. Гнучкість досягається, коли ви відкриєте щойно створений об’єкт клієнту свого класу, використовуючи більш абстрактний інтерфейс (фабрики та заводські методи - прекрасний приклад)
crizzis

"tl; dr. Використання дитини над батьків є кращим у локальному масштабі. Це не лише допомагає при читанні", - не обов'язково ... якщо ви не використовуєте дочірні специфіки, то це просто додасть більше деталей, ніж потрібно. "але також необхідно переконатися, що роздільна здатність перевантажених методів працює належним чином і допомагає забезпечити ефективну компіляцію." - це багато що залежить від мови програмування. Є багато, що не мають жодного питання, що виникає з цього приводу.
AnoE

1
Чи є у вас це джерело Parent p = new Child(); p.doSomething();і Child c = new Child; c.doSomething();маєте іншу поведінку? Можливо, це вигадка якоюсь мовою, але передбачається, що об'єкт контролює поведінку, а не посилання. Ось така краса спадщини! Поки ви можете довіряти дочірнім програмам для виконання контракту з батьківською реалізацією, ви готові йти.
corsiKa

@corsiKa Так, це можливо кількома способами. Два швидкі приклади - це, коли підписи методів перезаписуються, наприклад, із newключовим словом у C # та коли є декілька визначень, наприклад, із множинним успадкуванням у C ++ .
Нат

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

0

Враховуючи parentType foo = new childType1();, код буде обмежений використанням батьківського методу foo, але він зможе зберігати fooпосилання на будь-який об'єкт, тип якого походить від parent--не лише екземплярів childType1. Якщо новий childType1екземпляр - це єдине, на що fooколи-небудь матиме посилання, тоді може бути краще оголосити fooтип як childType1. З іншого боку, якщо, fooможливо, потрібно буде містити посилання на інші типи, якщо це буде оголошено як таке, це parentTypeстане можливим.


0

Я пропоную використовувати

Child x = new Child();

замість

Parent x = new Child();

Остання втрачає інформацію, тоді як перша ні.

Ви можете зробити все з першим, що можете зробити з останнім, але не навпаки.


Щоб детальніше розробити покажчик, давайте у вас є кілька рівнів успадкування.

Base-> DerivedL1->DerivedL2

І ви хочете ініціалізувати результат new DerivedL2()до змінної. Використання базового типу залишає вам можливість використання Baseабо DerivedL1.

Можна використовувати

Base x = new DerivedL2();

або

DerivedL1 x = new DerivedL2();

Я не бачу логічного способу віддати перевагу одному перед іншим.

Якщо ви використовуєте

DerivedL2 x = new DerivedL2();

сперечатися нема про що.


3
Вміти робити менше, якщо вам не потрібно робити більше - це гарна справа, ні?
Пітер

1
@ Петер, можливість робити менше не обмежується. Це завжди можливо. Якщо здатність робити більше обмежена, це може бути боляче.
R Sahu

Але втратити інформацію іноді - це добре. Що стосується вашої ієрархії, то більшість людей, ймовірно, проголосували б за Base(якщо це працює). Ви віддаєте перевагу найбільш конкретну, AFAIK більшість вважає за найменш конкретну. Я б сказав, що для локальних змінних це навряд чи має значення.
maaartinus

@maaartinus, я здивований, що більшість користувачів вважають за краще втрачати інформацію. Я не бачу переваг втрати інформації так чітко, як інші.
R Sahu

Коли ви заявляєте, Base x = new DerivedL2();то вас найбільше обмежує, і це дозволяє з мінімальними зусиллями переключити іншого (онука) дитини. Більше того, ви відразу бачите, що це можливо. +++ Чи згодні ми з цим, що void f(Base)краще void f(DerivedL2)?
maaartinus

0

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

Напр

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

Параметри є List<T>, дуже загального типу. ArrayList<T>, LinkedList<T>І інші все можуть бути прийняті за цим методом.

Важливо, що тип повернення - LinkedHashMap<K, V>ні Map<K, V>. Якщо хтось хоче призначити результат цього методу а Map<K, V>, він може це зробити. Це чітко повідомляє, що цей результат є не просто картою, а картою з певним упорядкуванням.

Припустимо, цей метод повернуто Map<K, V>. Якби абонент хотів LinkedHashMap<K, V>, щоб він мав зробити перевірку типу, передачу та обробку помилок, що справді громіздко.


Та сама логіка, яка змушує вас прийняти широкий тип як параметр, також повинна застосовуватися і до типу повернення. Якщо мета функції - зіставити ключі до значень, вона повинна повернути a Mapне a LinkedHashMap- ви хочете повернути LinkedHashMapлише функцію на зразок keyValuePairsToFifoMapчи щось таке. Ширше - краще, поки у вас немає конкретної причини цього не робити.
corsiKa

@corsiKa Хм, я згоден, це краще ім’я, але це лише один приклад. Я не погоджуюся з тим, що (загалом) розширення вашого типу повернення краще. Ваша штучна видалення інформації про тип, без жодних обґрунтувань. Якщо ви передбачаєте, що вашому користувачеві доведеться відмовитись від широкого типу, який ви їм надали, тоді ви зробили їм послугу.
Олександр - Відновіть Моніку

"без жодного виправдання" - але є виправдання! Якщо я можу піти від повернення ширшого типу, це полегшує рефакторинг вниз по дорозі. Якщо хтось потребує конкретного типу через певну поведінку на ньому, я все для повернення відповідного типу. Але я не хочу замикатися на конкретному типі, якщо мені цього не потрібно. Мені б хотілося вільно змінювати особливості мого методу, не завдаючи шкоди тим, хто його споживає, і якщо я його зміню, скажімо, LinkedHashMapз TreeMapбудь-якої причини, тепер у мене є багато чого, що потрібно змінити.
corsiKa

Власне, я згоден з цим аж до останнього речення. Ви перейшли з LinkedHashMapна TreeMapне тривіальну зміну, таку як перехід з ArrayListна LinkedList. Це зміна з семантичним значенням. У такому випадку ви хочете, щоб компілятор видав помилку, змусив споживачів повернутого значення помітити зміни та вжити відповідних заходів.
Олександр - Відновіть Моніку

Але це може бути тривіальною зміною, якщо все, що вам важливо, - це поведінка на карті (яка, якщо ви можете, ви повинні.) Якщо контракт "ви отримаєте ключову карту цінності", а замовлення не має значення (що справедливо для більшість карт), то не має значення, чи це карта дерева чи хешу. Наприклад, Thread#getAllStackTraces()- повертає Map<Thread,StackTraceElement[]>замість HashMapконкретно так у дорозі, що вони можуть змінити його на будь-який тип, який Mapвони хочуть.
corsiKa
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.