Является ли «Parent x = new Child ();» вместо «Child x = new Child ();» плохой практикой, если мы можем использовать последний?

32

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

Fragment myFragment=new MyFragment();

которая объявляет переменную как Fragment вместо MyFragment, а MyFragment является дочерним классом Fragment. Я не удовлетворен этой строкой кодов, потому что я думаю, что этот код должен быть:

MyFragment myFragment=new MyFragment();

что более конкретно, это правда?

Или в обобщении вопроса, это плохая практика для использования:

Parent x=new Child();

вместо того

Child x=new Child();

если мы можем изменить первый на второй без ошибки компиляции?

ggrr
источник
6
Если мы сделаем последнее, нет смысла абстрагироваться / обобщать. Конечно, есть исключения ...
Вальфрат
2
В проекте, над которым я работаю, мои функции ожидают родительский тип и запускают методы родительского типа. Я могу принять любого из нескольких дочерних элементов, и код этих (унаследованных и переопределенных) методов различен, но я хочу запускать этот код независимо от того, какой дочерний элемент я использую.
Стивен С
4
@Walfrat Действительно, в абстракции мало смысла, когда вы уже запускаете конкретный тип, но вы все равно можете вернуть абстрактный тип, чтобы клиентский код был в блаженном неведении.
Джейкоб Райле
4
Не используйте Родителя / Ребенка. Используйте интерфейс / класс (для Java).
Турбьёрн Равн Андерсен
4
Google "программирование в интерфейс" для кучу объяснений того, почему использование Parent x = new Child()является хорошей идеей.
Кевин Воркман

Ответы:

69

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

Примером может быть наличие LinkedListа, с ArrayListкоторого оба спускаются List. Если код будет одинаково хорошо работать с любым списком, то нет причин произвольно ограничивать его одним из подклассов.

JacquesB
источник
24
В точку. Чтобы добавить пример, подумайте о том List list = new ArrayList(), что код, использующий список, не заботится о том, что это за список, только тот, который соответствует контракту интерфейса List. В будущем мы могли бы легко перейти на другую реализацию List, и базовый код вообще не нуждался бы в изменении или обновлении.
Maybe_Factor
19
хороший ответ, но следует развернуть этот наиболее абстрактный тип из возможных, что означает, что код в области видимости не должен быть возвращен дочернему элементу для выполнения некоторой специфической дочерней операции.
Майк
10
@Mike: я надеюсь, что это само собой разумеется, в противном случае все переменные, параметры и поля будут объявлены как Object!
JacquesB
3
@JacquesB: поскольку этому вопросу и ответу уделялось так много внимания, я думал, что явное выражение будет полезно для людей с разными уровнями квалификации.
Майк
1
Это зависит от контекста, это не ответ.
Биллаль Бегерадж
11

Ответ JacquesB правильный, хотя и немного абстрактный. Позвольте мне подчеркнуть некоторые аспекты более четко:

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

Ситуация меняется, когда вы получаете этот предмет откуда-то еще, например IFragment fragment = SomeFactory.GiveMeFragments(...);. Здесь фабрика обычно должна возвращать максимально абстрактный тип, и нисходящий код не должен зависеть от деталей реализации.

Бернхард Хиллер
источник
18
«хоть немного абстрактно». Badumtish.
17
1
это не могло быть редактированием?
beppe9000
6

Потенциально существуют различия в производительности.

Если производный класс запечатан, то компилятор может встроить и оптимизировать или устранить то, что иначе было бы виртуальными вызовами. Так что может быть значительная разница в производительности.

Пример: ToStringэто виртуальный звонок, но Stringзапечатан. On String, ToStringэто no-op, поэтому, если вы объявляете, objectчто это виртуальный вызов, если вы объявляете a, Stringкомпилятор не знает, что производный класс не переопределил метод, потому что класс запечатан, так что это no-op. Аналогичные соображения применимы к ArrayListпротив LinkedList.

Следовательно, если вы знаете конкретный тип объекта (и нет причин для его скрытия), вы должны объявить этот тип. Поскольку вы только что создали объект, вы знаете конкретный тип.

Бен
источник
4
В большинстве случаев различия в производительности незначительны. И в большинстве случаев (я слышал, что это происходит примерно в 90% случаев), мы должны забыть об этих небольших различиях. Только тогда, когда мы узнаем (предпочтительно через измерение), что мы стареем как часть кода, критически важная для производительности, мы должны заботиться о такой оптимизации.
Жюль
И компилятор знает, что «new Child ()» возвращает Child, даже если он назначен для Parent, и, пока он знает, он может использовать эти знания и вызывать код Child ().
gnasher729
+1. Также украл твой пример в моем ответе. = P
Nat
1
Обратите внимание, что есть оптимизация производительности, которая делает все это спорным. Например, HotSpot заметит, что параметр, передаваемый в функцию, всегда относится к одному определенному классу, и соответственно оптимизируется. Если это предположение окажется ложным, метод деоптимизируется. Но это сильно зависит от среды компилятора / среды выполнения. В прошлый раз, когда я проверял, CLR не может даже выполнить довольно тривиальную оптимизацию, заметив, что p from 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
Почему я должен был читать страницы, чтобы найти лучший ответ? Рейтинг отстой.
Basilevs
Спасибо за добрые слова. У рейтинговых систем есть свои плюсы и минусы, и один из них заключается в том, что последующие ответы иногда могут остаться в пыли. Бывает!
CorsiKa
2

Вообще говоря, вы должны использовать

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

или

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

для локальных переменных, если только нет веской причины для переменной иметь тип, отличный от того, с чем она инициализируется.

(Здесь я игнорирую тот факт, что код C ++, скорее всего, должен использовать умный указатель. Есть случаи, когда нет и не более одной строки, которую я не могу знать на самом деле.)

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

Джошуа
источник
1
В C ++ переменные в основном создаются без нового ключевого слова: stackoverflow.com/questions/6500313/…
Marian
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 - использованиеChildoverParentпредпочтительно в локальной области. Это не только помогает с удобочитаемостью, но также необходимо гарантировать, что разрешение перегруженного метода работает правильно и помогает включить эффективную компиляцию.


В местном масштабе

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();имеют смысл, но для кого - то , видя код в первый раз, больше информации это просто полезна.

Кроме того, в зависимости от среды разработки, среда может быть в состоянии обеспечить более полезные подсказки и метаданные о 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в локальной области метода. ПроблемаParentvs.Childочень отличается для возвращаемых типов, аргументов и полей класса.

натуральный
источник
2
Я бы добавил, что Parent list = new Child()это не имеет особого смысла. Код, который напрямую создает конкретный экземпляр объекта (в отличие от косвенного обращения через фабрики, фабричные методы и т. Д.), Содержит точную информацию о создаваемом типе, ему может потребоваться взаимодействие с конкретным интерфейсом для настройки вновь созданного объекта и т. Д. Локальная сфера не там, где вы достигаете гибкости Гибкость достигается, когда вы открываете этот вновь созданный объект клиенту вашего класса, используя более абстрактный интерфейс (фабрики и фабричные методы - прекрасный пример)
crizzis
«Использование локального дочернего элемента над родителем предпочтительнее в локальной области. Это не только помогает с удобочитаемостью», - не обязательно ... если вы не используете специфику дочернего элемента, то он просто добавит больше деталей, чем необходимо. «но также необходимо убедиться, что разрешение перегруженного метода работает правильно и помогает включить эффективную компиляцию». - это во многом зависит от языка программирования. Есть много, которые не имеют никаких проблем, вытекающих из этого.
AnoE
1
Есть ли у вас есть источник , что Parent p = new Child(); p.doSomething();и Child c = new Child; c.doSomething();имеют различное поведение? Возможно, это какая-то странность в каком-то языке, но предполагается, что объект управляет поведением, а не ссылкой. В этом красота наследства! Пока вы можете доверять дочерним реализациям для выполнения контракта родительской реализации, все в порядке.
CorsiKa
@corsiKa Да, это возможно несколькими способами. Два быстрых примера: где сигнатуры методов перезаписываются, например, с newключевым словом в C # , и когда есть несколько определений, например, с множественным наследованием в C ++ .
Nat
@corsiKa Просто быстрый третий пример (потому что я думаю, что это четкий контраст между C ++ и C #), C # имеет проблему, подобную C ++, где вы также можете получить это поведение от методов явного интерфейса . Это забавно, потому что языки, такие как C #, используют интерфейсы вместо частичного наследования, чтобы избежать этих проблем, но, приняв интерфейсы, они все еще получили часть проблемы - но, это не так плохо, потому что в этом сценарии он применяется только когда Parentэто interface.
Nat
0

Учитывая parentType foo = new childType1();, код будет ограничен использованием методов родительского типа foo, но сможет хранить fooссылки на любой объект, тип которого происходит от - parentnot only экземпляров childType1. Если новый childType1экземпляр является единственной вещью, на которую fooкогда-либо будет храниться ссылка, тогда может быть лучше объявить fooтип как childType1. С другой стороны, если fooможет понадобиться хранить ссылки на другие типы, объявив его как parentType, сделает это возможным.

Supercat
источник
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
@ Петр, способность делать меньше не ограничена. Это всегда возможно. Если способность делать больше ограничена, это может быть больным вопросом.
Р Саху
Но потеря информации - это иногда хорошо. Что касается вашей иерархии, большинство людей, вероятно, проголосовали бы Base(если это работает). Вы предпочитаете самый конкретный, AFAIK большинство предпочитает наименее конкретный. Я бы сказал, что для локальных переменных это вряд ли имеет значение.
17
@maaartinus, я удивлен, что большинство пользователей предпочитают терять информацию. Я не вижу преимущества потери информации так же ясно, как другие.
Р Саху
Когда вы объявляете, 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>. Если кто-то хочет присвоить результат этого метода a Map<K, V>, он может это сделать. Это ясно показывает, что этот результат - больше, чем просто карта, но карта с определенным порядком.

Предположим, этот метод вернулся Map<K, V>. Если вызывающий объект хочет LinkedHashMap<K, V>, он должен будет выполнить проверку типов, приведение типов и обработку ошибок, что является действительно громоздким.

Александр - Восстановить Монику
источник
Та же логика, которая вынуждает вас принимать широкий тип в качестве параметра, должна также применяться к типу возвращаемого значения. Если цель функции состоит в том, чтобы сопоставить ключи со значениями, она должна возвращать Mapне a LinkedHashMap- вы хотели бы вернуть только LinkedHashMapдля функции, подобной keyValuePairsToFifoMapили чего-то подобного . Чем шире, тем лучше, пока у вас нет особых причин не делать этого.
CorsiKa
@ CorsiKa Хм Я согласен, что это лучшее имя, но это только один пример. Я не согласен с тем, что (в целом) расширение вашего типа возврата лучше. Вы искусственно удаляете информацию о типах без какого-либо обоснования. Если вы предполагаете, что вашему пользователю будет разумно необходимо уменьшить количество предоставленных им широких типов, то вы оказали им плохую услугу.
Александр - Восстановить Монику
«без всякого оправдания» - но оправдание есть! Если мне удастся вернуть более широкий тип, это облегчит рефакторинг в будущем. Если кому-то НУЖЕН определенный тип из-за некоторого поведения на нем, я полностью за возвращение соответствующего типа. Но я не хочу быть привязанным к определенному типу, если мне это не нужно. Я хотел бы свободно изменять специфику моего метода, не причиняя вреда тем, кто его потребляет, и если я изменю его, скажем, LinkedHashMapпо TreeMapкакой-либо причине, теперь у меня есть много вещей, которые можно изменить.
CorsiKa
На самом деле, я согласен с этим вплоть до последнего предложения. Вы изменили с LinkedHashMapна TreeMapне тривиальное изменение, такое как изменение с ArrayListна LinkedList. Это изменение с смысловой важностью. В этом случае вы хотите, чтобы компилятор выдавал ошибку, заставлял потребителей возвращаемого значения замечать изменения и предпринимать соответствующие действия.
Александр - Восстановить Монику
Но это может быть тривиальным изменением, если все, что вас волнует, это поведение карты (что, если вы можете, вы должны.) Если контракт «вы получите карту значения ключа» и порядок не имеет значения (что верно для большинство карт), тогда не имеет значения, является ли это дерево или хэш-карта. Например, Thread#getAllStackTraces()- возвращает Map<Thread,StackTraceElement[]>вместо HashMapопределенного, так что в будущем они могут изменить его на любой тип, который Mapони хотят.
CorsiKa