Например, я видел несколько кодов, которые создают такой фрагмент:
Fragment myFragment=new MyFragment();
которая объявляет переменную как Fragment вместо MyFragment, а MyFragment является дочерним классом Fragment. Я не удовлетворен этой строкой кодов, потому что я думаю, что этот код должен быть:
MyFragment myFragment=new MyFragment();
что более конкретно, это правда?
Или в обобщении вопроса, это плохая практика для использования:
Parent x=new Child();
вместо того
Child x=new Child();
если мы можем изменить первый на второй без ошибки компиляции?
Parent x = new Child()
является хорошей идеей.Ответы:
Это зависит от контекста, но я бы сказал, что вы должны объявить наиболее абстрактный тип из возможных. Таким образом, ваш код будет максимально общим и не будет зависеть от ненужных деталей.
Примером может быть наличие
LinkedList
а, сArrayList
которого оба спускаютсяList
. Если код будет одинаково хорошо работать с любым списком, то нет причин произвольно ограничивать его одним из подклассов.источник
List list = new ArrayList()
, что код, использующий список, не заботится о том, что это за список, только тот, который соответствует контракту интерфейса List. В будущем мы могли бы легко перейти на другую реализацию List, и базовый код вообще не нуждался бы в изменении или обновлении.Object
!Ответ JacquesB правильный, хотя и немного абстрактный. Позвольте мне подчеркнуть некоторые аспекты более четко:
В своем фрагменте кода вы явно используете
new
ключевое слово, и, возможно, вы хотели бы продолжить дальнейшие шаги инициализации, которые, вероятно, специфичны для конкретного типа: тогда вам нужно объявить переменную с этим конкретным конкретным типом.Ситуация меняется, когда вы получаете этот предмет откуда-то еще, например
IFragment fragment = SomeFactory.GiveMeFragments(...);
. Здесь фабрика обычно должна возвращать максимально абстрактный тип, и нисходящий код не должен зависеть от деталей реализации.источник
Потенциально существуют различия в производительности.
Если производный класс запечатан, то компилятор может встроить и оптимизировать или устранить то, что иначе было бы виртуальными вызовами. Так что может быть значительная разница в производительности.
Пример:
ToString
это виртуальный звонок, ноString
запечатан. OnString
,ToString
это no-op, поэтому, если вы объявляете,object
что это виртуальный вызов, если вы объявляете a,String
компилятор не знает, что производный класс не переопределил метод, потому что класс запечатан, так что это no-op. Аналогичные соображения применимы кArrayList
противLinkedList
.Следовательно, если вы знаете конкретный тип объекта (и нет причин для его скрытия), вы должны объявить этот тип. Поскольку вы только что создали объект, вы знаете конкретный тип.
источник
Parent p = new Child()
всегда ребенок ...Ключевым отличием является требуемый уровень доступа. И каждый хороший ответ об абстракции касается животных, или так мне говорят.
Допустим, у вас есть несколько животных -
Cat
Bird
иDog
. Теперь, эти животные имеют несколько общего поведения -move()
,eat()
иspeak()
. Все они едят по-разному и говорят по-разному, но если мне нужно, чтобы мое животное ело или говорило, мне все равно, как они это делают.Но иногда я делаю. У меня никогда не было сторожевого кота или сторожевой птицы, но у меня есть сторожевая собака. Поэтому, когда кто-то врывается в мой дом, я не могу просто положиться на разговор, чтобы отпугнуть злоумышленника. Мне действительно нужно, чтобы моя собака лаяла - это не то, что он говорит, а просто гав.
Так что в коде, который требует вторжения, мне действительно нужно сделать
Dog fido = getDog(); fido.barkMenancingly();
Но большую часть времени я могу с радостью заставлять животных делать трюки, где они лают, мяукают или пишут в твиттере.
Animal pet = getAnimal(); pet.speak();
Итак, чтобы быть более конкретным, у ArrayList есть некоторые методы, которых
List
нет. Следует отметить, чтоtrimToSize()
. Сейчас в 99% случаев нас это не волнует.List
Почти никогда не является достаточно большим для нас заботиться. Но бывают времена, когда это так. Поэтому время от времени, мы должны специально попросить ,ArrayList
чтобы выполнитьtrimToSize()
и сделать его поддержка массива меньше.Обратите внимание, что это не обязательно делать при построении - это можно сделать с помощью метода возврата или даже приведения, если мы абсолютно уверены.
источник
Вообще говоря, вы должны использовать
или
для локальных переменных, если только нет веской причины для переменной иметь тип, отличный от того, с чем она инициализируется.
(Здесь я игнорирую тот факт, что код C ++, скорее всего, должен использовать умный указатель. Есть случаи, когда нет и не более одной строки, которую я не могу знать на самом деле.)
Общая идея здесь заключается в языках, которые поддерживают автоматическое определение типов переменных, его использование облегчает чтение кода и делает правильные вещи (то есть система типов компилятора может максимально оптимизировать систему и изменить инициализацию на новую. Тип работает так же хорошо, если был использован наиболее абстрактный).
источник
Хорошо, потому что это легко понять и легко изменить этот код:
Хорошо, потому что он сообщает намерение:
Хорошо, потому что это распространено и легко понять:
Единственный вариант, который действительно плох - использовать
Parent
толькоChild
методDoSomething()
. Это плохо, потому что неправильно сообщает о намерениях:Теперь давайте подробнее рассмотрим случай, который конкретно задает вопрос:
Давайте отформатируем это по-другому, сделав вторую половину вызова функции:
Это может быть сокращено до:
Здесь я предполагаю, что общепризнанно, что это
WhichType
должен быть самый базовый / абстрактный тип, который позволяет функции работать (если не возникают проблемы с производительностью). В этом случае правильный тип будет либоParent
, либо что-тоParent
происходит от.Эта линия рассуждений объясняет, почему использование типа
Parent
является хорошим выбором, но это не влияет на погоду, другие варианты плохи (они не являются).источник
tl; dr - использование
Child
overParent
предпочтительно в локальной области. Это не только помогает с удобочитаемостью, но также необходимо гарантировать, что разрешение перегруженного метода работает правильно и помогает включить эффективную компиляцию.В местном масштабе
Концептуально, речь идет о поддержании максимально возможной информации. Если мы опустимся до
Parent
, мы, по сути, просто удаляем информацию о типах, которая могла бы быть полезной.Сохранение полной информации о типе имеет четыре основных преимущества:
Преимущество 1: дополнительная информация для компилятора
Видимый тип используется в разрешении перегруженного метода и в оптимизации.
Пример: разрешение перегруженного метода
В приведенном выше примере оба
foo()
вызова работают , но в одном случае мы получаем лучшее разрешение перегруженного метода.Пример: оптимизация компилятора
В приведенном выше примере оба
.Foo()
вызова в конечном итоге вызывают один и тот жеoverride
метод, который возвращает2
. Просто, в первом случае, есть поиск виртуального метода, чтобы найти правильный метод; этот поиск виртуального метода не требуется во втором случае, так как этот методsealed
.Благодарим @Ben, который привел аналогичный пример в своем ответе .
Преимущество 2: больше информации для читателя
Знание точного типа, т. Е.
Child
Предоставляет больше информации тому, кто читает код, облегчая понимание того, что делает программа.Конечно, может быть , это не имеет значения для фактического кода , поскольку оба
parent.Foo();
иchild.Foo();
имеют смысл, но для кого - то , видя код в первый раз, больше информации это просто полезна.Кроме того, в зависимости от среды разработки, среда может быть в состоянии обеспечить более полезные подсказки и метаданные о
Child
чемParent
.Преимущество 3: более чистый, более стандартизированный код
Большинство примеров кода на C #, которые я видел в последнее время, используют
var
, что в основном является сокращениемChild
.Видя, что
var
заявление не объявлено, просто выглядит отвратительно; если есть ситуативная причина для этого, круто, но в остальном это выглядит анти-паттерном.Преимущество 4: более изменчивая логика программы для прототипирования
Первые три преимущества были большими; этот гораздо более ситуативный.
Тем не менее, для людей, которые используют код, как другие используют Excel, мы постоянно меняем наш код. Может быть , нам не нужно вызывать метод , уникальный для
Child
в этой версии кода, но мы могли бы перепрофилировать или переработать код позже.Преимущество сильной системы типов состоит в том, что она предоставляет нам определенные метаданные о логике нашей программы, делая возможности более очевидными. Это невероятно полезно при создании прототипов, поэтому лучше хранить его там, где это возможно.
Резюме
Использование
Parent
нарушает разрешение перегруженного метода, запрещает некоторые оптимизации компилятора, удаляет информацию из читателя и делает код более уродливым.Использование
var
действительно путь. Это быстро, чисто, по шаблону и помогает компилятору и IDE правильно выполнять свою работу.Важный : этот ответ о
Parent
противChild
в локальной области метода. ПроблемаParent
vs.Child
очень отличается для возвращаемых типов, аргументов и полей класса.источник
Parent list = new Child()
это не имеет особого смысла. Код, который напрямую создает конкретный экземпляр объекта (в отличие от косвенного обращения через фабрики, фабричные методы и т. Д.), Содержит точную информацию о создаваемом типе, ему может потребоваться взаимодействие с конкретным интерфейсом для настройки вновь созданного объекта и т. Д. Локальная сфера не там, где вы достигаете гибкости Гибкость достигается, когда вы открываете этот вновь созданный объект клиенту вашего класса, используя более абстрактный интерфейс (фабрики и фабричные методы - прекрасный пример)Parent p = new Child(); p.doSomething();
иChild c = new Child; c.doSomething();
имеют различное поведение? Возможно, это какая-то странность в каком-то языке, но предполагается, что объект управляет поведением, а не ссылкой. В этом красота наследства! Пока вы можете доверять дочерним реализациям для выполнения контракта родительской реализации, все в порядке.new
ключевым словом в C # , и когда есть несколько определений, например, с множественным наследованием в C ++ .Parent
этоinterface
.Учитывая
parentType foo = new childType1();
, код будет ограничен использованием методов родительского типаfoo
, но сможет хранитьfoo
ссылки на любой объект, тип которого происходит от -parent
not only экземпляровchildType1
. Если новыйchildType1
экземпляр является единственной вещью, на которуюfoo
когда-либо будет храниться ссылка, тогда может быть лучше объявитьfoo
тип какchildType1
. С другой стороны, еслиfoo
может понадобиться хранить ссылки на другие типы, объявив его какparentType
, сделает это возможным.источник
Я предлагаю использовать
вместо того
Последний теряет информацию, а первый нет.
С первым вы можете делать все то же, что и со вторым, но не наоборот.
Для дальнейшей разработки указателя, давайте иметь несколько уровней наследования.
Base
->DerivedL1
->DerivedL2
И вы хотите инициализировать результат
new DerivedL2()
переменной. Использование базового типа оставляет вам возможность использоватьBase
илиDerivedL1
.Вы можете использовать
или
Я не вижу никакого логического способа отдать предпочтение одному другому.
Если вы используете
тут не о чем спорить.
источник
Base
(если это работает). Вы предпочитаете самый конкретный, AFAIK большинство предпочитает наименее конкретный. Я бы сказал, что для локальных переменных это вряд ли имеет значение.Base x = new DerivedL2();
вы больше всего ограничены, и это позволяет вам переключить другого (внушительного) ребенка с минимальными усилиями. Более того, вы сразу видите, что это возможно. +++ Согласны ли мы с тем, чтоvoid f(Base)
лучше чемvoid f(DerivedL2)
?Я бы предложил всегда возвращать или хранить переменные как наиболее специфический тип, но принимать параметры как самые широкие типы.
Например
Параметры
List<T>
очень общего типа.ArrayList<T>
,LinkedList<T>
И другие все могут быть приняты по этому методу.Важно отметить, что тип возвращаемого значения -
LinkedHashMap<K, V>
нетMap<K, V>
. Если кто-то хочет присвоить результат этого метода aMap<K, V>
, он может это сделать. Это ясно показывает, что этот результат - больше, чем просто карта, но карта с определенным порядком.Предположим, этот метод вернулся
Map<K, V>
. Если вызывающий объект хочетLinkedHashMap<K, V>
, он должен будет выполнить проверку типов, приведение типов и обработку ошибок, что является действительно громоздким.источник
Map
не aLinkedHashMap
- вы хотели бы вернуть толькоLinkedHashMap
для функции, подобнойkeyValuePairsToFifoMap
или чего-то подобного . Чем шире, тем лучше, пока у вас нет особых причин не делать этого.LinkedHashMap
поTreeMap
какой-либо причине, теперь у меня есть много вещей, которые можно изменить.LinkedHashMap
наTreeMap
не тривиальное изменение, такое как изменение сArrayList
наLinkedList
. Это изменение с смысловой важностью. В этом случае вы хотите, чтобы компилятор выдавал ошибку, заставлял потребителей возвращаемого значения замечать изменения и предпринимать соответствующие действия.Thread#getAllStackTraces()
- возвращаетMap<Thread,StackTraceElement[]>
вместоHashMap
определенного, так что в будущем они могут изменить его на любой тип, которыйMap
они хотят.