Na przykład widziałem niektóre kody, które tworzą taki fragment:
Fragment myFragment=new MyFragment();
który deklaruje zmienną jako Fragment zamiast MyFragment, który MyFragment jest klasą potomną Fragment. Nie jestem usatysfakcjonowany tą linią kodów, ponieważ uważam, że ten kod powinien być:
MyFragment myFragment=new MyFragment();
co jest bardziej szczegółowe, czy to prawda?
Lub w uogólnieniu pytania, czy złą praktyką jest stosowanie:
Parent x=new Child();
zamiast
Child x=new Child();
czy możemy zmienić poprzednią na drugą bez błędu kompilacji?
Parent x = new Child()
jest dobrym pomysłem.Odpowiedzi:
To zależy od kontekstu, ale argumentowałbym, że powinieneś zadeklarować najbardziej abstrakcyjny typ z możliwych. W ten sposób Twój kod będzie jak najbardziej ogólny i nie będzie zależał od nieistotnych szczegółów.
Przykładem może być posiadanie
LinkedList
i odArrayList
którego oba pochodząList
. Jeśli kod działałby równie dobrze z dowolnym rodzajem listy, nie ma powodu, aby arbitralnie ograniczać go do jednej z podklas.źródło
List list = new ArrayList()
Kod korzystający z listy nie dba o to, jaką to jest lista, tylko że spełnia umowę interfejsu List. W przyszłości moglibyśmy z łatwością przejść na inną implementację List, a kod źródłowy nie wymagałby zmiany ani aktualizacji.Object
!Odpowiedź JacquesB jest poprawna, choć trochę abstrakcyjna. Pozwolę sobie wyraźniej podkreślić niektóre aspekty:
W swoim fragmencie kodu wyraźnie używasz
new
słowa kluczowego i być może chciałbyś kontynuować dalsze etapy inicjalizacji, które prawdopodobnie są specyficzne dla konkretnego typu: wtedy musisz zadeklarować zmienną z tym konkretnym konkretnym typem.Sytuacja wygląda inaczej, gdy zdobędziesz ten przedmiot skądinąd, np
IFragment fragment = SomeFactory.GiveMeFragments(...);
. W tym przypadku fabryka powinna zwykle zwracać najbardziej abstrakcyjny typ, a kod w dół nie powinien zależeć od szczegółów implementacji.źródło
Istnieją potencjalnie różnice w wydajności.
Jeśli klasa pochodna jest zapieczętowana, kompilator może wstawić i zoptymalizować lub wyeliminować to, co w innym przypadku byłoby wirtualnymi wywołaniami. Dlatego może występować znacząca różnica w wydajności.
Przykład:
ToString
jest połączeniem wirtualnym, aleString
jest zapieczętowane. WłączString
,ToString
to brak operacji, więc jeśli deklarujesz jakoobject
wywołanie wirtualne, jeśli deklarujesz,String
że kompilator wie, że żadna klasa pochodna nie zastąpiła metody, ponieważ klasa jest zapieczętowana, więc nie ma opcji. Podobne rozważania dotycząArrayList
vsLinkedList
.Dlatego jeśli znasz konkretny typ obiektu (i nie ma powodu, by go ukryć), powinieneś zadeklarować ten typ. Ponieważ właśnie utworzyłeś obiekt, znasz typ betonu.
źródło
Parent p = new Child()
jest zawsze dzieckiem.Kluczową różnicą jest wymagany poziom dostępu. I każda dobra odpowiedź na temat abstrakcji dotyczy zwierząt, a przynajmniej tak mi powiedziano.
Załóżmy, że masz kilka zwierząt -
Cat
Bird
iDog
. Teraz, te zwierzęta mają kilka wspólnych zachowań -move()
,eat()
, ispeak()
. Wszystkie jedzą inaczej i mówią inaczej, ale jeśli potrzebuję, aby moje zwierzę je lub mówiło, nie obchodzi mnie, jak to robią.Ale czasami tak. Nigdy nie strzegłem kota stróżującego ani ptaka stróżującego, ale mam psa stróżującego. Kiedy więc ktoś włamuje się do mojego domu, nie mogę polegać na mówieniu, aby odstraszyć intruza. Naprawdę potrzebuję mojego psa do szczekania - to coś innego niż jego mowa, która jest po prostu dźwiękiem.
Więc w kodzie, który wymaga intruza, naprawdę muszę to zrobić
Dog fido = getDog(); fido.barkMenancingly();
Ale przez większość czasu mogę szczęśliwie nakłonić dowolne zwierzęta do robienia sztuczek, które przywołują, miauczą lub tweetują dla smakołyków.
Animal pet = getAnimal(); pet.speak();
Aby być bardziej konkretnym, ArrayList ma kilka metod, które
List
tego nie robią. Warto zauważyć, żetrimToSize()
. Teraz w 99% przypadków nie przejmujemy się tym. ToList
prawie nigdy nie jest wystarczająco duże, abyśmy się tym przejmowali. Ale są chwile, kiedy to jest. Od czasu do czasu musimy konkretnie poprosić oArrayList
wykonanietrimToSize()
i zmniejszenie jego tablicy pomocniczej.Pamiętaj, że nie trzeba tego robić podczas budowy - można to zrobić za pomocą metody zwrotu lub nawet obsady, jeśli jesteśmy absolutnie pewni.
źródło
Ogólnie rzecz biorąc, powinieneś użyć
lub
dla zmiennych lokalnych, chyba że istnieje uzasadniony powód, aby zmienna miała inny typ niż to, z którym została zainicjowana.
(Tutaj ignoruję fakt, że kod C ++ najprawdopodobniej powinien używać inteligentnego wskaźnika. Są przypadki, w których nie można i nie ma więcej niż jednej linii, której tak naprawdę nie znam).
Ogólna idea polega na tym, że języki obsługują automatyczne wykrywanie zmiennych, użycie go ułatwia odczyt kodu i robi właściwą rzecz (system kompilatora może zoptymalizować jak najwięcej i zmienić inicjalizację na nową typ działa również, jeśli użyto najbardziej abstrakcyjnych).
źródło
Dobrze, ponieważ łatwo jest zrozumieć i łatwo zmodyfikować ten kod:
Dobrze, ponieważ komunikuje zamiar:
Ok, ponieważ jest to powszechne i łatwe do zrozumienia:
Parent
Jedyną opcją, która jest naprawdę zła, jest użycie, jeśli tylkoChild
ma metodęDoSomething()
. Jest źle, ponieważ źle komunikuje zamiar:Teraz rozwiążmy trochę więcej na temat sprawy, o którą pyta konkretnie:
Sformatujmy to inaczej, tworząc drugą połowę wywołania funkcji:
Można to skrócić do:
Tutaj zakładam, że jest to powszechnie akceptowane
WhichType
powinien być najbardziej podstawowym / abstrakcyjnym typem, który pozwala na działanie tej funkcji (chyba że występują problemy z wydajnością). W tym przypadku właściwym typem będzie alboParent
, albo cośParent
pochodzi.Ten tok rozumowania wyjaśnia, dlaczego użycie tego typu
Parent
jest dobrym wyborem, ale nie zmienia się w pogodę, inne opcje są złe (nie są).źródło
tl; dr - Używanie opcji
Child
overParent
jest lepsze w zakresie lokalnym. Pomaga to nie tylko w czytelności, ale także zapewnia prawidłowe działanie przeciążonej rozdzielczości metody i umożliwia wydajną kompilację.W zakresie lokalnym
Koncepcyjnie chodzi o zachowanie jak największej ilości informacji o typie. Jeśli obniżymy do
Parent
, zasadniczo usuwamy informacje o typie, które mogłyby być przydatne.Zachowanie pełnej informacji o typie ma cztery główne zalety:
Zaleta 1: Więcej informacji dla kompilatora
Typ pozorny jest wykorzystywany przy przeładowaniu rozdzielczości i optymalizacji.
Przykład: Przeciążona rozdzielczość metody
W powyższym przykładzie oba
foo()
wywołania działają , ale w jednym przypadku uzyskujemy lepszą rozdzielczość przeciążonej metody.Przykład: optymalizacja kompilatora
W powyższym przykładzie oba
.Foo()
wywołania ostatecznie wywołują tę samąoverride
metodę, która zwraca2
. Po prostu w pierwszym przypadku istnieje wirtualne wyszukiwanie metod w celu znalezienia właściwej metody; to wyszukiwanie wirtualne metody nie jest potrzebne w drugim przypadku, ponieważ ta metoda jestsealed
.Podziękowania dla @Ben, który podał podobny przykład w swojej odpowiedzi .
Zaleta 2: Więcej informacji dla czytelnika
Znajomość dokładnego typu, tj.
Child
, Zapewnia więcej informacji każdemu, kto czyta kod, ułatwiając zobaczenie, co robi program.Oczywiście, może to nie ma znaczenia, do rzeczywistego kodu ponieważ oba
parent.Foo();
ichild.Foo();
sensu, ale dla kogoś, widząc kodu po raz pierwszy, więcej informacji znajduje się po prostu pomocne.Dodatkowo, w zależności od środowiska programistycznego, IDE może być w stanie dostarczyć bardziej pomocnych podpowiedzi i metadanych na temat
Child
niżParent
.Zaleta 3: Czystszy, bardziej znormalizowany kod
Większość przykładów kodu C #, które ostatnio widziałem
var
, jest w skrócie skrótemChild
.Widząc
var
oświadczenie o braku deklaracji po prostu wygląda; jeśli ma to uzasadnienie sytuacyjne, to świetnie, ale poza tym wygląda anty-wzór.Zaleta 4: Bardziej zmienna logika programu do prototypowania
Pierwsze trzy zalety były duże; ten jest o wiele bardziej sytuacyjny.
Jednak dla osób, które używają kodu tak jak inni używają Excela, stale zmieniamy nasz kod. Może nie musimy wywołać metodę unikalnego
Child
w tej wersji kodu, ale możemy modyfikowanie lub przerobienie kodu później.Zaletą silnego systemu typów jest to, że zapewnia nam pewne metadane dotyczące logiki naszego programu, dzięki czemu możliwości są bardziej widoczne. Jest to niezwykle przydatne w prototypowaniu, więc najlepiej zachować go tam, gdzie to możliwe.
Podsumowanie
Używanie
Parent
bałaganu przy przeciążonej rozdzielczości metody, hamuje niektóre optymalizacje kompilatora, usuwa informacje z czytnika i sprawia, że kod jest brzydszy.Używanie
var
jest naprawdę właściwą drogą. Jest szybki, czysty, wzorowany i pomaga kompilatorowi i IDE poprawnie wykonywać swoje zadania.Ważne : Ta odpowiedź dotyczy
Parent
porównaniaChild
w lokalnym zasięgu metody. KwestiaParent
vs.Child
jest bardzo różna dla typów zwracanych, argumentów i pól klas.źródło
Parent list = new Child()
to nie ma większego sensu. Kod, który bezpośrednio tworzy konkretną instancję obiektu (w przeciwieństwie do pośrednictwa przez fabryki, metody fabryczne itp.) Zawiera doskonałe informacje o tworzonym typie, może wymagać interakcji z konkretnym interfejsem, aby skonfigurować nowo utworzony obiekt itp. Zasięg lokalny nie zależy od elastyczności. Elastyczność osiąga się, gdy ujawnisz ten nowo utworzony obiekt klientowi swojej klasy za pomocą bardziej abstrakcyjnego interfejsu (fabryki i metody fabryczne są doskonałym przykładem)Parent p = new Child(); p.doSomething();
iChild c = new Child; c.doSomething();
mają różne zachowanie? Może to dziwactwo w jakimś języku, ale przypuszczalnie obiekt kontroluje zachowanie, a nie odniesienie. To jest piękno dziedzictwa! Tak długo, jak możesz zaufać implementacjom potomnym w celu wypełnienia kontraktu implementacji macierzystej, możesz iść.new
Słowem kluczowym w C # i gdy istnieje wiele definicji, np. Z wielokrotnym dziedziczeniem w C ++ .Parent
jestinterface
.Biorąc pod uwagę
parentType foo = new childType1();
, kod będzie ograniczony do używania metod typu nadrzędnegofoo
, ale będzie mógł przechowywać wfoo
referencji do dowolnego obiektu, którego typ pochodziparent
- nie tylko od instancjichildType1
. Jeśli nowachildType1
instancja jest jedyną rzeczą, do którejfoo
kiedykolwiek będzie się odwoływać, może lepiej zadeklarowaćfoo
typ jakochildType1
. Z drugiej strony, jeślifoo
zajdzie potrzeba przechowywania odniesień do innych typów, zadeklarowanie go jakoparentType
umożliwi to.źródło
Sugeruję użycie
zamiast
Ten ostatni traci informacje, podczas gdy pierwszy nie.
Możesz zrobić wszystko z tym pierwszym, co możesz zrobić z tym drugim, ale nie odwrotnie.
Aby rozwinąć wskaźnik dalej, pozwól nam mieć wiele poziomów dziedziczenia.
Base
->DerivedL1
->DerivedL2
I chcesz zainicjować wynik
new DerivedL2()
zmiennej. Użycie typu podstawowego pozostawia opcję użyciaBase
lubDerivedL1
.Możesz użyć
lub
Nie widzę żadnego logicznego sposobu na preferowanie jednego nad drugim.
Jeśli użyjesz
nie ma się o co kłócić.
źródło
Base
(zakładając, że to działa). Wolisz najbardziej szczegółowe, AFAIK większość preferuje najmniej szczegółowe. Powiedziałbym, że w przypadku zmiennych lokalnych nie ma to większego znaczenia.Base x = new DerivedL2();
jesteś najbardziej ograniczony, a to pozwala ci na włączenie innego (wnuczkiego) dziecka przy minimalnym wysiłku. Co więcej, od razu widać, że jest to możliwe. +++ Czy zgadzamy się, żevoid f(Base)
jest to lepsze niżvoid f(DerivedL2)
?Sugerowałbym zawsze zwracanie lub przechowywanie zmiennych jako najbardziej specyficzny typ, ale przyjmowanie parametrów jako najszerszych typów.
Na przykład
Parametry są
List<T>
bardzo ogólne.ArrayList<T>
,LinkedList<T>
I wszyscy inni mogą zostać zaakceptowane przez tą metodą.Co ważne, typem zwrotu jest
LinkedHashMap<K, V>
nieMap<K, V>
. Jeśli ktoś chce przypisać wynik tej metody do aMap<K, V>
, może to zrobić. Wyraźnie informuje, że ten wynik to więcej niż mapa, ale mapa ze zdefiniowanym porządkiem.Załóżmy, że ta metoda zwróciła
Map<K, V>
. Jeśli dzwoniący chciałbyLinkedHashMap<K, V>
, musiałby sprawdzić typ, obsłużyć i obsługiwać błędy, co jest naprawdę kłopotliwe.źródło
Map
nieLinkedHashMap
- chciałbyś tylko zwrócić funkcjęLinkedHashMap
dla funkcji podobnejkeyValuePairsToFifoMap
lub coś w tym rodzaju. Szersze jest lepsze, dopóki nie masz konkretnego powodu, aby tego nie robić.LinkedHashMap
zTreeMap
jakiegokolwiek powodu, teraz mam wiele rzeczy do zmiany.LinkedHashMap
naTreeMap
nie jest trywialną zmianą, taką jak zmiana zArrayList
naLinkedList
. To zmiana o znaczeniu semantycznym. W takim przypadku chcesz, aby kompilator zgłosił błąd, aby zmusić odbiorców wartości zwracanej do zauważenia zmiany i podjęcia odpowiednich działań.Thread#getAllStackTraces()
- zwracaMap<Thread,StackTraceElement[]>
zamiastHashMap
specyficznie, więc w dół drogi mogą zmienić go na dowolny rodzaj,Map
jaki chcą.